nodefony 7.0.2 → 10.0.0-alpha.2
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/.ai/symbols.json +36135 -0
- package/LICENSE +544 -0
- package/README.md +316 -694
- package/bin/nodefony +2183 -85
- package/dist/client/Container.js +367 -0
- package/dist/client/Event.js +198 -0
- package/dist/client/Service.js +344 -0
- package/dist/client/Tools.js +162 -0
- package/dist/client/client/ClientKernel.js +276 -0
- package/dist/client/client/angular/index.js +352 -0
- package/dist/client/client/announce.js +212 -0
- package/dist/client/client/debugbar/DebugBar.js +1624 -0
- package/dist/client/client/debugbar/format.js +103 -0
- package/dist/client/client/debugbar/hmr.js +37 -0
- package/dist/client/client/debugbar/index.js +29 -0
- package/dist/client/client/debugbar/model.js +133 -0
- package/dist/client/client/debugbar/network.js +173 -0
- package/dist/client/client/debugbar/profile.js +114 -0
- package/dist/client/client/index.js +34 -0
- package/dist/client/client/react/index.js +268 -0
- package/dist/client/client/realtime/AdaptiveRate.js +240 -0
- package/dist/client/client/realtime/BrowserWsTransport.js +57 -0
- package/dist/client/client/realtime/RealtimeClient.js +898 -0
- package/dist/client/client/realtime/localEvents.js +56 -0
- package/dist/client/client/realtime/notice.js +125 -0
- package/dist/client/client/realtime/observe.js +280 -0
- package/dist/client/client/roles/index.js +3 -0
- package/dist/client/client/roles/registry.js +97 -0
- package/dist/client/client/roles/roles.js +91 -0
- package/dist/client/client/shim/events.js +88 -0
- package/dist/client/client/shim/util.js +20 -0
- package/dist/client/client/svelte/index.js +319 -0
- package/dist/client/client/syslog/context.js +122 -0
- package/dist/client/client/syslog/errors.js +49 -0
- package/dist/client/client/syslog/uplink.js +106 -0
- package/dist/client/client/vue/index.js +321 -0
- package/dist/client/colors.js +63 -0
- package/dist/client/debugbar.standalone.js +4274 -0
- package/dist/client/realtime/IRealtimeTransport.js +32 -0
- package/dist/client/realtime/JsonRpcPeer.js +251 -0
- package/dist/client/realtime/channelRate.js +39 -0
- package/dist/client/realtime/platformChannels.js +155 -0
- package/dist/client/syslog/Pdu.js +224 -0
- package/dist/client/syslog/Syslog.js +1159 -0
- package/dist/client/syslog/drivers/pduFlow.js +106 -0
- package/dist/client/syslog/drivers/pduProtocol.js +28 -0
- package/dist/client/syslog/logColor.js +80 -0
- package/dist/client/types/src/Container.d.ts +221 -0
- package/dist/client/types/src/Event.d.ts +171 -0
- package/dist/client/types/src/FileClass.d.ts +184 -0
- package/dist/client/types/src/Service.d.ts +165 -0
- package/dist/client/types/src/Tools.d.ts +116 -0
- package/dist/client/types/src/client/ClientKernel.d.ts +90 -0
- package/dist/client/types/src/client/IClientKernel.d.ts +195 -0
- package/dist/client/types/src/client/angular/index.d.ts +273 -0
- package/dist/client/types/src/client/announce.d.ts +74 -0
- package/dist/client/types/src/client/debugbar/DebugBar.d.ts +279 -0
- package/dist/client/types/src/client/debugbar/format.d.ts +48 -0
- package/dist/client/types/src/client/debugbar/hmr.d.ts +16 -0
- package/dist/client/types/src/client/debugbar/index.d.ts +30 -0
- package/dist/client/types/src/client/debugbar/model.d.ts +158 -0
- package/dist/client/types/src/client/debugbar/network.d.ts +55 -0
- package/dist/client/types/src/client/debugbar/profile.d.ts +99 -0
- package/dist/client/types/src/client/index.d.ts +47 -0
- package/dist/client/types/src/client/react/index.d.ts +207 -0
- package/dist/client/types/src/client/realtime/AdaptiveRate.d.ts +138 -0
- package/dist/client/types/src/client/realtime/BrowserWsTransport.d.ts +27 -0
- package/dist/client/types/src/client/realtime/RealtimeClient.d.ts +573 -0
- package/dist/client/types/src/client/realtime/localEvents.d.ts +53 -0
- package/dist/client/types/src/client/realtime/notice.d.ts +70 -0
- package/dist/client/types/src/client/realtime/observe.d.ts +284 -0
- package/dist/client/types/src/client/roles/index.d.ts +15 -0
- package/dist/client/types/src/client/roles/registry.d.ts +72 -0
- package/dist/client/types/src/client/roles/roles.d.ts +68 -0
- package/dist/client/types/src/client/shim/cli-color.d.ts +2 -0
- package/dist/client/types/src/client/shim/events.d.ts +39 -0
- package/dist/client/types/src/client/shim/util.d.ts +14 -0
- package/dist/client/types/src/client/svelte/index.d.ts +216 -0
- package/dist/client/types/src/client/syslog/context.d.ts +76 -0
- package/dist/client/types/src/client/syslog/errors.d.ts +46 -0
- package/dist/client/types/src/client/syslog/uplink.d.ts +89 -0
- package/dist/client/types/src/client/transport/websocket.d.ts +5 -0
- package/dist/client/types/src/client/vue/index.d.ts +234 -0
- package/dist/client/types/src/colors.d.ts +32 -0
- package/dist/client/types/src/config/infra.d.ts +171 -0
- package/dist/client/types/src/kernel/bootReport.d.ts +112 -0
- package/dist/client/types/src/kernel/readinessRegistry.d.ts +104 -0
- package/dist/client/types/src/realtime/IRealtimeSocket.d.ts +142 -0
- package/dist/client/types/src/realtime/IRealtimeTransport.d.ts +50 -0
- package/dist/client/types/src/realtime/JsonRpcPeer.d.ts +251 -0
- package/dist/client/types/src/realtime/RealtimeEventMap.d.ts +269 -0
- package/dist/client/types/src/realtime/channelRate.d.ts +55 -0
- package/dist/client/types/src/realtime/platformChannels.d.ts +147 -0
- package/dist/client/types/src/syslog/Pdu.d.ts +134 -0
- package/dist/client/types/src/syslog/Syslog.d.ts +482 -0
- package/dist/client/types/src/syslog/drivers/ILogDriver.d.ts +177 -0
- package/dist/client/types/src/syslog/drivers/pduFlow.d.ts +53 -0
- package/dist/client/types/src/syslog/drivers/pduProtocol.d.ts +20 -0
- package/dist/client/types/src/syslog/logColor.d.ts +59 -0
- package/dist/client/types/src/types/ICliKernel.d.ts +28 -0
- package/dist/client/types/src/types/ICommand.d.ts +14 -0
- package/dist/client/types/src/types/IContainer.d.ts +35 -0
- package/dist/client/types/src/types/IKernel.d.ts +138 -0
- package/dist/client/types/src/types/IMcpTool.d.ts +157 -0
- package/dist/client/types/src/types/IModule.d.ts +73 -0
- package/dist/client/types/src/types/IPage.d.ts +119 -0
- package/dist/client/types/src/types/IService.d.ts +62 -0
- package/dist/client/types/src/types/ISyslog.d.ts +58 -0
- package/dist/client/types/src/types/ITransport.d.ts +5 -0
- package/dist/client/types/src/types/globals.d.ts +47 -0
- package/dist/node/Cli.js +750 -0
- package/dist/node/Container.js +367 -0
- package/dist/node/Error.js +309 -0
- package/dist/node/Event.js +198 -0
- package/dist/node/FileClass.js +308 -0
- package/dist/node/Nodefony.js +78 -0
- package/dist/node/Service.js +344 -0
- package/dist/node/Tools.js +205 -0
- package/dist/node/bin/resolveLocalCli.js +10 -0
- package/dist/node/bundler/index.js +103 -0
- package/dist/node/cli/agentTargets.js +461 -0
- package/dist/node/cli/aiMcp.js +553 -0
- package/dist/node/cli/aiMcpReport.js +129 -0
- package/dist/node/cli/aiSync.js +323 -0
- package/dist/node/cli/aiSyncReport.js +142 -0
- package/dist/node/cli/card.js +186 -0
- package/dist/node/cli/cardReport.js +110 -0
- package/dist/node/cli/completion.js +415 -0
- package/dist/node/cli/create.js +805 -0
- package/dist/node/cli/env.js +332 -0
- package/dist/node/cli/envReport.js +159 -0
- package/dist/node/cli/execPortable.js +60 -0
- package/dist/node/cli/gitHooks.js +191 -0
- package/dist/node/cli/gitHooksReport.js +119 -0
- package/dist/node/cli/globalFlags.js +67 -0
- package/dist/node/cli/helpReport.js +288 -0
- package/dist/node/cli/manPage.js +165 -0
- package/dist/node/cli/nodefonyBin.js +61 -0
- package/dist/node/cli/outdated.js +144 -0
- package/dist/node/cli/progress.js +555 -0
- package/dist/node/cli/projectRoot.js +32 -0
- package/dist/node/cli/promptPassword.js +44 -0
- package/dist/node/cli/prompts.js +86 -0
- package/dist/node/cli/scaffold/destination.js +92 -0
- package/dist/node/cli/scaffold/engine.js +2156 -0
- package/dist/node/cli/scaffold/entityFields.js +589 -0
- package/dist/node/cli/scaffold/format.js +243 -0
- package/dist/node/cli/scaffold/interactive.js +124 -0
- package/dist/node/cli/scaffold/moduleLayout.js +38 -0
- package/dist/node/cli/scaffold/reservedEntities.js +74 -0
- package/dist/node/cli/scaffold/spec.js +704 -0
- package/dist/node/cli/scaffold/steps.js +40 -0
- package/dist/node/cli/scaffold/userContractSource.js +133 -0
- package/dist/node/cli/scaffold/versions.js +57 -0
- package/dist/node/cli/scaffold/writer.js +172 -0
- package/dist/node/cli/startMenu.js +496 -0
- package/dist/node/cli/symbols.js +205 -0
- package/dist/node/cli/sysexits.js +34 -0
- package/dist/node/cli/tableReport.js +184 -0
- package/dist/node/cli/usageReport.js +144 -0
- package/dist/node/colors.js +63 -0
- package/dist/node/command/Builder.js +153 -0
- package/dist/node/command/Command.js +348 -0
- package/dist/node/config/configProvenance.js +101 -0
- package/dist/node/config/defaults.js +39 -0
- package/dist/node/config/defineConfig.js +129 -0
- package/dist/node/config/defineEnv.js +264 -0
- package/dist/node/config/envExample.js +41 -0
- package/dist/node/config/envOverride.js +423 -0
- package/dist/node/config/infra.js +163 -0
- package/dist/node/config/reactivity.js +23 -0
- package/dist/node/config/reservedEnv.js +118 -0
- package/dist/node/config/schema.js +134 -0
- package/dist/node/config/use.js +24 -0
- package/dist/node/finder/File.js +49 -0
- package/dist/node/finder/FileResult.js +86 -0
- package/dist/node/finder/Finder.js +201 -0
- package/dist/node/finder/Result.js +45 -0
- package/dist/node/index.js +121 -0
- package/dist/node/kernel/BootConfigurationError.js +54 -0
- package/dist/node/kernel/CliKernel.js +630 -0
- package/dist/node/kernel/Kernel.js +2198 -0
- package/dist/node/kernel/Module.js +453 -0
- package/dist/node/kernel/adminPlane/adminCaller.js +98 -0
- package/dist/node/kernel/adminPlane/adminRbac.js +51 -0
- package/dist/node/kernel/adminPlane/catalog.js +158 -0
- package/dist/node/kernel/adminPlane/executeAdmin.js +78 -0
- package/dist/node/kernel/checks/deep.js +335 -0
- package/dist/node/kernel/checks/freshness.js +139 -0
- package/dist/node/kernel/checks/gating.js +231 -0
- package/dist/node/kernel/checks/guards.js +232 -0
- package/dist/node/kernel/checks/lastBoot.js +136 -0
- package/dist/node/kernel/checks/live.js +330 -0
- package/dist/node/kernel/checks/packageDeps.js +292 -0
- package/dist/node/kernel/checks/readiness.js +153 -0
- package/dist/node/kernel/checks/renderReport.js +780 -0
- package/dist/node/kernel/checks/report.js +421 -0
- package/dist/node/kernel/checks/runDoctor.js +731 -0
- package/dist/node/kernel/checks/surface.js +364 -0
- package/dist/node/kernel/checks/walk.js +100 -0
- package/dist/node/kernel/checks/wiring.js +450 -0
- package/dist/node/kernel/commands/AiMcpCommand.js +65 -0
- package/dist/node/kernel/commands/AiSyncCommand.js +48 -0
- package/dist/node/kernel/commands/BuildCommand.js +49 -0
- package/dist/node/kernel/commands/CardCommand.js +50 -0
- package/dist/node/kernel/commands/ClusterCommand.js +58 -0
- package/dist/node/kernel/commands/CompletionCommand.js +31 -0
- package/dist/node/kernel/commands/CreateCommand.js +45 -0
- package/dist/node/kernel/commands/DevCommand.js +99 -0
- package/dist/node/kernel/commands/DoctorCommand.js +115 -0
- package/dist/node/kernel/commands/EnvCommand.js +52 -0
- package/dist/node/kernel/commands/GitHooksCommand.js +47 -0
- package/dist/node/kernel/commands/InspectCommand.js +153 -0
- package/dist/node/kernel/commands/InstallCommand.js +20 -0
- package/dist/node/kernel/commands/MenuCommand.js +261 -0
- package/dist/node/kernel/commands/OutdatedCommand.js +164 -0
- package/dist/node/kernel/commands/ProdCommand.js +60 -0
- package/dist/node/kernel/commands/StatusCommand.js +30 -0
- package/dist/node/kernel/commands/StopCommand.js +35 -0
- package/dist/node/kernel/commands/SymbolsCommand.js +45 -0
- package/dist/node/kernel/commands/runtimeLauncher.js +95 -0
- package/dist/node/kernel/decorators/kernelDecorator.js +110 -0
- package/dist/node/kernel/injector/injector.js +137 -0
- package/dist/node/kernel/injector/serviceOrder.js +106 -0
- package/dist/node/kernel/inspect/adminSubjects.js +194 -0
- package/dist/node/kernel/inspect/docOutline.js +86 -0
- package/dist/node/kernel/lifecycleTags.js +76 -0
- package/dist/node/kernel/moduleConfig.js +75 -0
- package/dist/node/kernel/moduleGating.js +68 -0
- package/dist/node/kernel/readinessRegistry.js +98 -0
- package/dist/node/kernel/resolveModuleEntry.js +61 -0
- package/dist/node/mcp/caller.js +40 -0
- package/dist/node/mcp/guard.js +51 -0
- package/dist/node/mcp/protocol.js +157 -0
- package/dist/node/mcp/server.js +216 -0
- package/dist/node/mcp/tools.js +861 -0
- package/dist/node/oauth/authorizationServer.js +221 -0
- package/dist/node/oauth/protectedResource.js +293 -0
- package/dist/node/package.js +4 -0
- package/dist/node/realtime/IRealtimeTransport.js +32 -0
- package/dist/node/realtime/JsonRpcPeer.js +251 -0
- package/dist/node/realtime/RealtimeEventMap.js +15 -0
- package/dist/node/realtime/channelRate.js +39 -0
- package/dist/node/realtime/platformChannels.js +155 -0
- package/dist/node/runtime/GcScheduler.js +92 -0
- package/dist/node/runtime/RequestContext.js +76 -0
- package/dist/node/runtime/bearer.js +84 -0
- package/dist/node/runtime/engineEnvironment.js +79 -0
- package/dist/node/runtime/loadEnv.js +105 -0
- package/dist/node/runtime/pageFacets.js +85 -0
- package/dist/node/runtime/pageFilters.js +103 -0
- package/dist/node/runtime/pageGuard.js +84 -0
- package/dist/node/runtime/pageQuery.js +160 -0
- package/dist/node/runtime/pageSort.js +114 -0
- package/dist/node/runtime/redact.js +43 -0
- package/dist/node/runtime/withTimeout.js +71 -0
- package/dist/node/service/cluster/ClusterManager.js +202 -0
- package/dist/node/service/cluster/ClusterProbeAggregator.js +120 -0
- package/dist/node/service/cluster/ClusterRelay.js +74 -0
- package/dist/node/service/cluster/clusterMaster.js +65 -0
- package/dist/node/service/cluster/clusterMessage.js +70 -0
- package/dist/node/service/cluster/cpuQuota.js +59 -0
- package/dist/node/service/cluster/instanceProbe.js +52 -0
- package/dist/node/service/cluster/podEnvironment.js +48 -0
- package/dist/node/service/cluster/processProbe.js +70 -0
- package/dist/node/service/cluster/richProcessProbe.js +115 -0
- package/dist/node/service/cluster/topology.js +71 -0
- package/dist/node/service/dev/BootReporter.js +490 -0
- package/dist/node/service/dev/DevSupervisor.js +1004 -0
- package/dist/node/service/dev/bootVerdict.js +158 -0
- package/dist/node/service/dev/detachedStart.js +372 -0
- package/dist/node/service/dev/devProcess.js +1137 -0
- package/dist/node/service/dev/devProjects.js +138 -0
- package/dist/node/service/dev/devStatusReport.js +395 -0
- package/dist/node/service/dev/devStop.js +263 -0
- package/dist/node/service/fetchService.js +35 -0
- package/dist/node/service/gitService.js +62 -0
- package/dist/node/syslog/Pdu.js +224 -0
- package/dist/node/syslog/Syslog.js +1159 -0
- package/dist/node/syslog/drivers/ClusterFileLogDriver.js +114 -0
- package/dist/node/syslog/drivers/FileLogDriver.js +127 -0
- package/dist/node/syslog/drivers/ILogDriver.js +26 -0
- package/dist/node/syslog/drivers/LokiLogDriver.js +144 -0
- package/dist/node/syslog/drivers/MemoryLogDriver.js +33 -0
- package/dist/node/syslog/drivers/OpenSearchLogDriver.js +159 -0
- package/dist/node/syslog/drivers/builtinLogDrivers.js +114 -0
- package/dist/node/syslog/drivers/filterPdus.js +71 -0
- package/dist/node/syslog/drivers/logDriverRegistry.js +83 -0
- package/dist/node/syslog/drivers/opensearchShared.js +17 -0
- package/dist/node/syslog/drivers/pduFlow.js +106 -0
- package/dist/node/syslog/drivers/pduProtocol.js +28 -0
- package/dist/node/syslog/httpFetch.js +43 -0
- package/dist/node/syslog/logColor.js +80 -0
- package/dist/node/syslog/sinks/FileSink.js +142 -0
- package/dist/node/syslog/transports/BatchingHttpTransport.js +98 -0
- package/dist/node/syslog/transports/ConsoleTransport.js +14 -0
- package/dist/node/syslog/transports/FileTransport.js +17 -0
- package/dist/node/syslog/transports/HttpTransport.js +44 -0
- package/dist/node/syslog/transports/LokiTransport.js +80 -0
- package/dist/node/syslog/transports/OpenSearchTransport.js +53 -0
- package/dist/node/syslog/transports/SyslogTransport.js +13 -0
- package/dist/node/syslog/transports/index.js +8 -0
- package/dist/node/testing/index.js +190 -0
- package/dist/types/Cli.d.ts +177 -0
- package/dist/types/Container.d.ts +221 -0
- package/dist/types/Error.d.ts +162 -0
- package/dist/types/Event.d.ts +171 -0
- package/dist/types/FileClass.d.ts +184 -0
- package/dist/types/Nodefony.d.ts +66 -0
- package/dist/types/Service.d.ts +165 -0
- package/dist/types/Tools.d.ts +116 -0
- package/dist/types/bin/resolveLocalCli.d.ts +49 -0
- package/dist/types/bundler/index.d.ts +66 -0
- package/dist/types/cli/agentTargets.d.ts +378 -0
- package/dist/types/cli/aiMcp.d.ts +239 -0
- package/dist/types/cli/aiMcpReport.d.ts +113 -0
- package/dist/types/cli/aiSync.d.ts +68 -0
- package/dist/types/cli/aiSyncReport.d.ts +149 -0
- package/dist/types/cli/card.d.ts +49 -0
- package/dist/types/cli/cardReport.d.ts +127 -0
- package/dist/types/cli/completion.d.ts +154 -0
- package/dist/types/cli/create.d.ts +177 -0
- package/dist/types/cli/env.d.ts +103 -0
- package/dist/types/cli/envReport.d.ts +153 -0
- package/dist/types/cli/execPortable.d.ts +54 -0
- package/dist/types/cli/gitHooks.d.ts +48 -0
- package/dist/types/cli/gitHooksReport.d.ts +101 -0
- package/dist/types/cli/globalFlags.d.ts +48 -0
- package/dist/types/cli/helpReport.d.ts +110 -0
- package/dist/types/cli/manPage.d.ts +21 -0
- package/dist/types/cli/nodefonyBin.d.ts +45 -0
- package/dist/types/cli/outdated.d.ts +119 -0
- package/dist/types/cli/progress.d.ts +349 -0
- package/dist/types/cli/projectRoot.d.ts +19 -0
- package/dist/types/cli/promptPassword.d.ts +22 -0
- package/dist/types/cli/prompts.d.ts +102 -0
- package/dist/types/cli/scaffold/destination.d.ts +70 -0
- package/dist/types/cli/scaffold/engine.d.ts +545 -0
- package/dist/types/cli/scaffold/entityFields.d.ts +238 -0
- package/dist/types/cli/scaffold/format.d.ts +78 -0
- package/dist/types/cli/scaffold/interactive.d.ts +11 -0
- package/dist/types/cli/scaffold/moduleLayout.d.ts +69 -0
- package/dist/types/cli/scaffold/reservedEntities.d.ts +49 -0
- package/dist/types/cli/scaffold/spec.d.ts +159 -0
- package/dist/types/cli/scaffold/steps.d.ts +24 -0
- package/dist/types/cli/scaffold/userContractSource.d.ts +86 -0
- package/dist/types/cli/scaffold/versions.d.ts +14 -0
- package/dist/types/cli/scaffold/writer.d.ts +100 -0
- package/dist/types/cli/startMenu.d.ts +195 -0
- package/dist/types/cli/symbols.d.ts +84 -0
- package/dist/types/cli/sysexits.d.ts +31 -0
- package/dist/types/cli/tableReport.d.ts +44 -0
- package/dist/types/cli/usageReport.d.ts +108 -0
- package/dist/types/client/ClientKernel.d.ts +90 -0
- package/dist/types/client/IClientKernel.d.ts +195 -0
- package/dist/types/client/angular/index.d.ts +273 -0
- package/dist/types/client/announce.d.ts +74 -0
- package/dist/types/client/debugbar/DebugBar.d.ts +279 -0
- package/dist/types/client/debugbar/format.d.ts +48 -0
- package/dist/types/client/debugbar/hmr.d.ts +16 -0
- package/dist/types/client/debugbar/index.d.ts +30 -0
- package/dist/types/client/debugbar/model.d.ts +158 -0
- package/dist/types/client/debugbar/network.d.ts +55 -0
- package/dist/types/client/debugbar/profile.d.ts +99 -0
- package/dist/types/client/index.d.ts +47 -0
- package/dist/types/client/react/index.d.ts +207 -0
- package/dist/types/client/realtime/AdaptiveRate.d.ts +138 -0
- package/dist/types/client/realtime/BrowserWsTransport.d.ts +27 -0
- package/dist/types/client/realtime/RealtimeClient.d.ts +573 -0
- package/dist/types/client/realtime/localEvents.d.ts +53 -0
- package/dist/types/client/realtime/notice.d.ts +70 -0
- package/dist/types/client/realtime/observe.d.ts +284 -0
- package/dist/types/client/roles/index.d.ts +15 -0
- package/dist/types/client/roles/registry.d.ts +72 -0
- package/dist/types/client/roles/roles.d.ts +68 -0
- package/dist/types/client/shim/cli-color.d.ts +2 -0
- package/dist/types/client/shim/events.d.ts +39 -0
- package/dist/types/client/shim/util.d.ts +14 -0
- package/dist/types/client/svelte/index.d.ts +216 -0
- package/dist/types/client/syslog/context.d.ts +76 -0
- package/dist/types/client/syslog/errors.d.ts +46 -0
- package/dist/types/client/syslog/uplink.d.ts +89 -0
- package/dist/types/client/transport/websocket.d.ts +5 -0
- package/dist/types/client/vue/index.d.ts +234 -0
- package/dist/types/colors.d.ts +32 -0
- package/dist/types/command/Builder.d.ts +47 -0
- package/dist/types/command/Command.d.ts +268 -0
- package/dist/types/config/configMeta.d.ts +53 -0
- package/dist/types/config/configProvenance.d.ts +58 -0
- package/dist/types/config/defaults.d.ts +33 -0
- package/dist/types/config/defineConfig.d.ts +70 -0
- package/dist/types/config/defineEnv.d.ts +170 -0
- package/dist/types/config/envExample.d.ts +23 -0
- package/dist/types/config/envOverride.d.ts +236 -0
- package/dist/types/config/index.d.ts +23 -0
- package/dist/types/config/infra.d.ts +171 -0
- package/dist/types/config/reactivity.d.ts +36 -0
- package/dist/types/config/reservedEnv.d.ts +145 -0
- package/dist/types/config/schema.d.ts +113 -0
- package/dist/types/config/types.d.ts +374 -0
- package/dist/types/config/use.d.ts +80 -0
- package/dist/types/finder/File.d.ts +26 -0
- package/dist/types/finder/FileResult.d.ts +14 -0
- package/dist/types/finder/Finder.d.ts +51 -0
- package/dist/types/finder/Result.d.ts +10 -0
- package/dist/types/index.d.ts +205 -0
- package/dist/types/kernel/BootConfigurationError.d.ts +50 -0
- package/dist/types/kernel/CliKernel.d.ts +289 -0
- package/dist/types/kernel/Kernel.d.ts +1255 -0
- package/dist/types/kernel/Module.d.ts +326 -0
- package/dist/types/kernel/adminPlane/adminCaller.d.ts +105 -0
- package/dist/types/kernel/adminPlane/adminRbac.d.ts +44 -0
- package/dist/types/kernel/adminPlane/catalog.d.ts +122 -0
- package/dist/types/kernel/adminPlane/executeAdmin.d.ts +99 -0
- package/dist/types/kernel/bootReport.d.ts +112 -0
- package/dist/types/kernel/checks/deep.d.ts +122 -0
- package/dist/types/kernel/checks/freshness.d.ts +49 -0
- package/dist/types/kernel/checks/gating.d.ts +134 -0
- package/dist/types/kernel/checks/guards.d.ts +57 -0
- package/dist/types/kernel/checks/lastBoot.d.ts +189 -0
- package/dist/types/kernel/checks/live.d.ts +110 -0
- package/dist/types/kernel/checks/packageDeps.d.ts +44 -0
- package/dist/types/kernel/checks/readiness.d.ts +97 -0
- package/dist/types/kernel/checks/renderReport.d.ts +107 -0
- package/dist/types/kernel/checks/report.d.ts +414 -0
- package/dist/types/kernel/checks/runDoctor.d.ts +361 -0
- package/dist/types/kernel/checks/surface.d.ts +148 -0
- package/dist/types/kernel/checks/walk.d.ts +45 -0
- package/dist/types/kernel/checks/wiring.d.ts +54 -0
- package/dist/types/kernel/commands/AiMcpCommand.d.ts +39 -0
- package/dist/types/kernel/commands/AiSyncCommand.d.ts +28 -0
- package/dist/types/kernel/commands/BuildCommand.d.ts +13 -0
- package/dist/types/kernel/commands/CardCommand.d.ts +34 -0
- package/dist/types/kernel/commands/ClusterCommand.d.ts +29 -0
- package/dist/types/kernel/commands/CompletionCommand.d.ts +17 -0
- package/dist/types/kernel/commands/CreateCommand.d.ts +16 -0
- package/dist/types/kernel/commands/DevCommand.d.ts +40 -0
- package/dist/types/kernel/commands/DoctorCommand.d.ts +55 -0
- package/dist/types/kernel/commands/EnvCommand.d.ts +35 -0
- package/dist/types/kernel/commands/GitHooksCommand.d.ts +27 -0
- package/dist/types/kernel/commands/InspectCommand.d.ts +59 -0
- package/dist/types/kernel/commands/InstallCommand.d.ts +7 -0
- package/dist/types/kernel/commands/MenuCommand.d.ts +58 -0
- package/dist/types/kernel/commands/OutdatedCommand.d.ts +47 -0
- package/dist/types/kernel/commands/ProdCommand.d.ts +29 -0
- package/dist/types/kernel/commands/StatusCommand.d.ts +17 -0
- package/dist/types/kernel/commands/StopCommand.d.ts +17 -0
- package/dist/types/kernel/commands/SymbolsCommand.d.ts +29 -0
- package/dist/types/kernel/commands/runtimeLauncher.d.ts +76 -0
- package/dist/types/kernel/decorators/kernelDecorator.d.ts +76 -0
- package/dist/types/kernel/injector/injector.d.ts +47 -0
- package/dist/types/kernel/injector/serviceOrder.d.ts +29 -0
- package/dist/types/kernel/inspect/adminSubjects.d.ts +169 -0
- package/dist/types/kernel/inspect/docOutline.d.ts +53 -0
- package/dist/types/kernel/lifecycleTags.d.ts +79 -0
- package/dist/types/kernel/moduleConfig.d.ts +35 -0
- package/dist/types/kernel/moduleGating.d.ts +78 -0
- package/dist/types/kernel/readinessRegistry.d.ts +104 -0
- package/dist/types/kernel/resolveModuleEntry.d.ts +46 -0
- package/dist/types/mcp/caller.d.ts +55 -0
- package/dist/types/mcp/guard.d.ts +66 -0
- package/dist/types/mcp/protocol.d.ts +163 -0
- package/dist/types/mcp/server.d.ts +70 -0
- package/dist/types/mcp/tools.d.ts +209 -0
- package/dist/types/oauth/authorizationServer.d.ts +204 -0
- package/dist/types/oauth/protectedResource.d.ts +339 -0
- package/dist/types/realtime/IRealtimeSocket.d.ts +142 -0
- package/dist/types/realtime/IRealtimeTransport.d.ts +50 -0
- package/dist/types/realtime/JsonRpcPeer.d.ts +251 -0
- package/dist/types/realtime/RealtimeEventMap.d.ts +269 -0
- package/dist/types/realtime/channelRate.d.ts +55 -0
- package/dist/types/realtime/platformChannels.d.ts +147 -0
- package/dist/types/runtime/GcScheduler.d.ts +96 -0
- package/dist/types/runtime/RequestContext.d.ts +152 -0
- package/dist/types/runtime/bearer.d.ts +88 -0
- package/dist/types/runtime/engineEnvironment.d.ts +115 -0
- package/dist/types/runtime/loadEnv.d.ts +86 -0
- package/dist/types/runtime/pageFacets.d.ts +104 -0
- package/dist/types/runtime/pageFilters.d.ts +139 -0
- package/dist/types/runtime/pageGuard.d.ts +78 -0
- package/dist/types/runtime/pageQuery.d.ts +138 -0
- package/dist/types/runtime/pageSort.d.ts +86 -0
- package/dist/types/runtime/redact.d.ts +27 -0
- package/dist/types/runtime/withTimeout.d.ts +51 -0
- package/dist/types/service/cluster/ClusterManager.d.ts +112 -0
- package/dist/types/service/cluster/ClusterProbeAggregator.d.ts +68 -0
- package/dist/types/service/cluster/ClusterRelay.d.ts +57 -0
- package/dist/types/service/cluster/clusterMaster.d.ts +40 -0
- package/dist/types/service/cluster/clusterMessage.d.ts +94 -0
- package/dist/types/service/cluster/cpuQuota.d.ts +37 -0
- package/dist/types/service/cluster/instanceProbe.d.ts +82 -0
- package/dist/types/service/cluster/podEnvironment.d.ts +35 -0
- package/dist/types/service/cluster/processProbe.d.ts +53 -0
- package/dist/types/service/cluster/richProcessProbe.d.ts +77 -0
- package/dist/types/service/cluster/topology.d.ts +64 -0
- package/dist/types/service/dev/BootReporter.d.ts +67 -0
- package/dist/types/service/dev/DevSupervisor.d.ts +117 -0
- package/dist/types/service/dev/bootVerdict.d.ts +99 -0
- package/dist/types/service/dev/detachedStart.d.ts +164 -0
- package/dist/types/service/dev/devProcess.d.ts +638 -0
- package/dist/types/service/dev/devProjects.d.ts +121 -0
- package/dist/types/service/dev/devStatusReport.d.ts +141 -0
- package/dist/types/service/dev/devStop.d.ts +57 -0
- package/dist/types/service/fetchService.d.ts +29 -0
- package/dist/types/service/gitService.d.ts +24 -0
- package/dist/types/syslog/Pdu.d.ts +134 -0
- package/dist/types/syslog/Syslog.d.ts +482 -0
- package/dist/types/syslog/drivers/ClusterFileLogDriver.d.ts +40 -0
- package/dist/types/syslog/drivers/FileLogDriver.d.ts +55 -0
- package/dist/types/syslog/drivers/ILogDriver.d.ts +177 -0
- package/dist/types/syslog/drivers/LokiLogDriver.d.ts +44 -0
- package/dist/types/syslog/drivers/MemoryLogDriver.d.ts +20 -0
- package/dist/types/syslog/drivers/OpenSearchLogDriver.d.ts +40 -0
- package/dist/types/syslog/drivers/builtinLogDrivers.d.ts +43 -0
- package/dist/types/syslog/drivers/filterPdus.d.ts +21 -0
- package/dist/types/syslog/drivers/logDriverRegistry.d.ts +100 -0
- package/dist/types/syslog/drivers/opensearchShared.d.ts +11 -0
- package/dist/types/syslog/drivers/pduFlow.d.ts +53 -0
- package/dist/types/syslog/drivers/pduProtocol.d.ts +20 -0
- package/dist/types/syslog/httpFetch.d.ts +48 -0
- package/dist/types/syslog/logColor.d.ts +59 -0
- package/dist/types/syslog/sinks/FileSink.d.ts +78 -0
- package/dist/types/syslog/transports/BatchingHttpTransport.d.ts +59 -0
- package/dist/types/syslog/transports/ConsoleTransport.d.ts +8 -0
- package/dist/types/syslog/transports/FileTransport.d.ts +13 -0
- package/dist/types/syslog/transports/HttpTransport.d.ts +15 -0
- package/dist/types/syslog/transports/LokiTransport.d.ts +45 -0
- package/dist/types/syslog/transports/OpenSearchTransport.d.ts +40 -0
- package/dist/types/syslog/transports/SyslogTransport.d.ts +9 -0
- package/dist/types/syslog/transports/index.d.ts +12 -0
- package/dist/types/testing/index.d.ts +197 -0
- package/dist/types/types/IAdminApi.d.ts +246 -0
- package/dist/types/types/ICliKernel.d.ts +28 -0
- package/dist/types/types/ICommand.d.ts +14 -0
- package/dist/types/types/IContainer.d.ts +35 -0
- package/dist/types/types/IIdempotencyStore.d.ts +159 -0
- package/dist/types/types/IKernel.d.ts +138 -0
- package/dist/types/types/IMcpTool.d.ts +157 -0
- package/dist/types/types/IModule.d.ts +73 -0
- package/dist/types/types/IModuleManifest.d.ts +49 -0
- package/dist/types/types/IPage.d.ts +119 -0
- package/dist/types/types/IService.d.ts +62 -0
- package/dist/types/types/ISyslog.d.ts +58 -0
- package/dist/types/types/ITransport.d.ts +5 -0
- package/dist/types/types/globals.d.ts +47 -0
- package/docs/angular-services.md +318 -0
- package/docs/catalogue.md +184 -0
- package/docs/cli.md +369 -0
- package/docs/client.md +616 -0
- package/docs/debugbar.md +218 -0
- package/docs/environnement.md +308 -0
- package/docs/index.md +388 -0
- package/docs/kernel.md +670 -0
- package/docs/progression.md +228 -0
- package/docs/react-hooks.md +686 -0
- package/docs/request-context.md +507 -0
- package/docs/service.md +590 -0
- package/docs/svelte-reactivite.md +293 -0
- package/docs/syslog.md +848 -0
- package/docs/testing.md +203 -0
- package/docs/vue-composables.md +255 -0
- package/man/nodefony.1 +152 -0
- package/package.json +163 -83
- package/templates/app/agents/AGENTS.md.tpl +967 -0
- package/templates/app/agents/POINTEUR.md.tpl +12 -0
- package/templates/app/base/Dockerfile.tpl +104 -0
- package/templates/app/base/README.md.tpl +314 -0
- package/templates/app/base/dockerignore.tpl +28 -0
- package/templates/app/base/env.ts.tpl +130 -0
- package/templates/app/base/gitattributes.tpl +28 -0
- package/templates/app/base/github/workflows/ci.yml.tpl +91 -0
- package/templates/app/base/gitignore.tpl +46 -0
- package/templates/app/base/gitlab-ci.yml.tpl +42 -0
- package/templates/app/base/index.ts.tpl +69 -0
- package/templates/app/base/nodefony.config.ts.tpl +342 -0
- package/templates/app/base/oxlintrc.json.tpl +127 -0
- package/templates/app/base/package.json.tpl +75 -0
- package/templates/app/base/prettierignore.tpl +24 -0
- package/templates/app/base/prettierrc.json.tpl +12 -0
- package/templates/app/base/rolldown.config.ts.tpl +8 -0
- package/templates/app/base/tests/config.test.ts.tpl +29 -0
- package/templates/app/base/tests/e2e.setup.ts.tpl +181 -0
- package/templates/app/base/tests/e2e.test.ts.tpl +105 -0
- package/templates/app/base/tests/migrations.e2e.test.ts.tpl +579 -0
- package/templates/app/base/tsconfig.json.tpl +47 -0
- package/templates/app/base/vitest.config.ts.tpl +88 -0
- package/templates/app/base/vitest.e2e.config.ts.tpl +39 -0
- package/templates/app/complete/compose.yaml.tpl +328 -0
- package/templates/app/complete/deploy/migrate-job.yaml.tpl +100 -0
- package/templates/app/complete/docker/db/init-nodefony-e2e.sql.tpl +30 -0
- package/templates/app/complete/docker/grafana/provisioning/datasources/loki.yaml.tpl +15 -0
- package/templates/app/complete/env.local.tpl +11 -0
- package/templates/app/complete/env.tpl +53 -0
- package/templates/app/complete/nodefony/entity/User.ts.tpl +69 -0
- package/templates/app/complete/nodefony/security/provisionUsers.ts.tpl +124 -0
- package/templates/app/complete/nodefony/service/AppBannerService.ts.tpl +98 -0
- package/templates/app/complete/nodefony/service/AppInfoService.ts.tpl +85 -0
- package/templates/app/frontend/angular/frontend/src/accent.css.tpl +34 -0
- package/templates/app/frontend/angular/frontend/src/app/app.component.ts.tpl +400 -0
- package/templates/app/frontend/react/frontend/src/App.tsx.tpl +421 -0
- package/templates/app/frontend/react/frontend/src/accent.css.tpl +30 -0
- package/templates/app/frontend/shared/frontend/src/brand.ts.tpl +3 -0
- package/templates/app/frontend/shared/frontend/src/showcase.css.tpl +224 -0
- package/templates/app/frontend/shared/nodefony/controllers/AppController.ts.tpl +45 -0
- package/templates/app/frontend/svelte/frontend/src/App.svelte.tpl +371 -0
- package/templates/app/frontend/svelte/frontend/src/accent.css.tpl +34 -0
- package/templates/app/frontend/vue/frontend/src/App.vue.tpl +367 -0
- package/templates/app/frontend/vue/frontend/src/accent.css.tpl +34 -0
- package/templates/app/home/nodefony/controllers/HomeController.ts.tpl +38 -0
- package/templates/command/nodefony/command/__NAME__.ts.tpl +132 -0
- package/templates/controller/duplex/nodefony/controllers/__NAME__.ts.tpl +188 -0
- package/templates/controller/example/nodefony/controllers/__NAME__.ts.tpl +348 -0
- package/templates/controller/hello/nodefony/controllers/__NAME__.ts.tpl +143 -0
- package/templates/controller/realtime/nodefony/controllers/__NAME__.ts.tpl +153 -0
- package/templates/controller/realtime/tests/__KEBAB__-realtime.test.ts.tpl +79 -0
- package/templates/controller/rest/nodefony/controllers/__NAME__.ts.tpl +159 -0
- package/templates/entity/base/nodefony/entity/__PASCAL__.schema.ts.tpl +31 -0
- package/templates/entity/base/nodefony/entity/__PASCAL__.ts.tpl +68 -0
- package/templates/entity/controller/nodefony/controllers/__PASCAL__Controller.ts.tpl +335 -0
- package/templates/entity/service/nodefony/service/__PASCAL__Service.ts.tpl +126 -0
- package/templates/entity/tests/tests/__KEBAB__.e2e.test.ts.tpl +358 -0
- package/templates/entity/tests/tests/__KEBAB__.test.ts.tpl +87 -0
- package/templates/front/angular/frontend/src/app/app.component.ts.tpl +30 -0
- package/templates/front/base/nodefony/controllers/__NAME__.ts.tpl +60 -0
- package/templates/front/react/frontend/src/App.tsx.tpl +26 -0
- package/templates/front/svelte/frontend/src/App.svelte.tpl +19 -0
- package/templates/front/vue/frontend/src/App.vue.tpl +23 -0
- package/templates/module/ai/AGENTS.md.tpl +71 -0
- package/templates/module/base/README.md.tpl +62 -0
- package/templates/module/base/docs/index.md.tpl +55 -0
- package/templates/module/base/index.ts.tpl +50 -0
- package/templates/module/base/nodefony/config/config.ts.tpl +60 -0
- package/templates/module/base/nodefony/config/defineModuleConfig.ts.tpl +55 -0
- package/templates/module/base/nodefony/src/errors/__PASCAL__Error.ts.tpl +20 -0
- package/templates/module/base/package.json.tpl +53 -0
- package/templates/module/base/rolldown.config.ts.tpl +14 -0
- package/templates/module/base/tests/__KEBAB__.test.ts.tpl +39 -0
- package/templates/module/base/tsconfig.json.tpl +42 -0
- package/templates/module/base/vitest.config.ts.tpl +27 -0
- package/templates/module/packages/CLAUDE.md.tpl +51 -0
- package/templates/module/packages/MEMORY.md.tpl +39 -0
- package/templates/module/packages/tsconfig.declarations.json.tpl +19 -0
- package/templates/module/packages/tsconfig.tests.json.tpl +11 -0
- package/templates/module/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +14 -0
- package/templates/module/service/nodefony/interfaces/index.ts.tpl +1 -0
- package/templates/module/service/nodefony/service/__PASCAL__Service.ts.tpl +116 -0
- package/templates/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +19 -0
- package/templates/service/nodefony/service/__PASCAL__Service.ts.tpl +136 -0
- package/templates/service/tests/__PASCAL__Service.test.ts.tpl +53 -0
- package/templates/shared/front-entry/angular/frontend/src/main.ts.tpl +17 -0
- package/templates/shared/front-entry/react/frontend/src/main.tsx.tpl +11 -0
- package/templates/shared/front-entry/svelte/frontend/src/main.ts.tpl +13 -0
- package/templates/shared/front-entry/vue/frontend/src/main.ts.tpl +12 -0
- package/templates/shared/front-registrar/nodefony/frontend/register__PASCAL__Entry.ts.tpl +40 -0
- package/templates/shared/front-shell/frontend/index.html.tpl +39 -0
- package/templates/shared/ng-app-tsconfig/frontend/tsconfig.app.json.tpl +18 -0
- package/templates/shared/svelte-shim/frontend/src/env.d.ts.tpl +12 -0
- package/templates/shared/vue-shim/frontend/src/env.d.ts.tpl +15 -0
- package/.yarnclean +0 -45
- package/CHANGELOG.md +0 -406
- package/autoloader.es6 +0 -253
- package/builder.es6 +0 -366
- package/cli/builder/bundles/bundle.js +0 -328
- package/cli/builder/bundles/controller.js +0 -73
- package/cli/builder/bundles/users-bundle/.yarnclean +0 -45
- package/cli/builder/bundles/users-bundle/Command/fixtureTask.js +0 -47
- package/cli/builder/bundles/users-bundle/Command/usersCommand.js +0 -105
- package/cli/builder/bundles/users-bundle/Entity/mongoose/userEntity.js +0 -168
- package/cli/builder/bundles/users-bundle/Entity/sequelize/userEntity.js +0 -231
- package/cli/builder/bundles/users-bundle/Fixtures/users.js +0 -117
- package/cli/builder/bundles/users-bundle/Fixtures/usersFixtures.js +0 -114
- package/cli/builder/bundles/users-bundle/Resources/config/config.js +0 -90
- package/cli/builder/bundles/users-bundle/Resources/config/routing.js +0 -17
- package/cli/builder/bundles/users-bundle/Resources/config/security.js +0 -96
- package/cli/builder/bundles/users-bundle/Resources/config/services.js +0 -7
- package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.dev.config.js +0 -13
- package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.prod.config.js +0 -31
- package/cli/builder/bundles/users-bundle/Resources/config/webpack.config.js +0 -146
- package/cli/builder/bundles/users-bundle/Resources/js/users.js +0 -153
- package/cli/builder/bundles/users-bundle/Resources/public/favicon.ico +0 -0
- package/cli/builder/bundles/users-bundle/Resources/public/images/users-logo.png +0 -0
- package/cli/builder/bundles/users-bundle/Resources/scss/awesome/font-awesome.config.js +0 -11
- package/cli/builder/bundles/users-bundle/Resources/scss/custom.scss +0 -23
- package/cli/builder/bundles/users-bundle/Resources/scss/users.scss +0 -59
- package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/login.js +0 -235
- package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/users.js +0 -232
- package/cli/builder/bundles/users-bundle/Resources/translations/login.en_en.yml +0 -14
- package/cli/builder/bundles/users-bundle/Resources/translations/login.fr_fr.yml +0 -14
- package/cli/builder/bundles/users-bundle/Resources/translations/users.en_en.yml +0 -35
- package/cli/builder/bundles/users-bundle/Resources/translations/users.fr_fr.yml +0 -36
- package/cli/builder/bundles/users-bundle/Resources/views/base.html.twig +0 -32
- package/cli/builder/bundles/users-bundle/Resources/views/footer.html.twig +0 -7
- package/cli/builder/bundles/users-bundle/Resources/views/header.html.twig +0 -90
- package/cli/builder/bundles/users-bundle/Resources/views/login/login.html.twig +0 -43
- package/cli/builder/bundles/users-bundle/Resources/views/users/createUser.html.twig +0 -296
- package/cli/builder/bundles/users-bundle/Resources/views/users/readUsers.html.twig +0 -94
- package/cli/builder/bundles/users-bundle/controller/api/graphql/graphqlController.js +0 -56
- package/cli/builder/bundles/users-bundle/controller/api/graphql/userResolver.js +0 -121
- package/cli/builder/bundles/users-bundle/controller/api/graphql/usertype.js +0 -58
- package/cli/builder/bundles/users-bundle/controller/api/openapi/loginApiController.js +0 -210
- package/cli/builder/bundles/users-bundle/controller/api/openapi/restUserController.js +0 -244
- package/cli/builder/bundles/users-bundle/controller/loginController.js +0 -91
- package/cli/builder/bundles/users-bundle/controller/usersController.js +0 -332
- package/cli/builder/bundles/users-bundle/package.json +0 -58
- package/cli/builder/bundles/users-bundle/readme.md +0 -360
- package/cli/builder/bundles/users-bundle/services/usersService.js +0 -411
- package/cli/builder/bundles/users-bundle/src/providers/mongoose/userProvider.js +0 -47
- package/cli/builder/bundles/users-bundle/src/providers/sequelize/userProvider.js +0 -48
- package/cli/builder/bundles/users-bundle/tests/loginTest.js +0 -182
- package/cli/builder/bundles/users-bundle/usersBundle.js +0 -26
- package/cli/builder/microService/microService.js +0 -261
- package/cli/builder/microService/skeletons/.editorconfig +0 -18
- package/cli/builder/microService/skeletons/.env-cmdrc.js +0 -32
- package/cli/builder/microService/skeletons/.jshintrc +0 -37
- package/cli/builder/microService/skeletons/README.md +0 -117
- package/cli/builder/microService/skeletons/bin/bash/hello.sh +0 -5
- package/cli/builder/microService/skeletons/bin/cli +0 -13
- package/cli/builder/microService/skeletons/bin/python/hello.py +0 -20
- package/cli/builder/microService/skeletons/config/config.js +0 -29
- package/cli/builder/microService/skeletons/config/pm2.config.js +0 -72
- package/cli/builder/microService/skeletons/config/webpack/webpack.config.dev.js +0 -8
- package/cli/builder/microService/skeletons/config/webpack/webpack.config.prod.js +0 -25
- package/cli/builder/microService/skeletons/config/webpack.config.js +0 -114
- package/cli/builder/microService/skeletons/package.json +0 -76
- package/cli/builder/microService/skeletons/src/browser/index.css +0 -3
- package/cli/builder/microService/skeletons/src/browser/index.js +0 -16
- package/cli/builder/microService/skeletons/src/browser/socketio.js +0 -46
- package/cli/builder/microService/skeletons/src/cli/cli.js +0 -224
- package/cli/builder/microService/skeletons/src/cli/menu.js +0 -28
- package/cli/builder/microService/skeletons/src/n-api/README.md +0 -1
- package/cli/builder/microService/skeletons/src/n-api/binding.gyp +0 -14
- package/cli/builder/microService/skeletons/src/n-api/hello.cc +0 -14
- package/cli/builder/microService/skeletons/src/n-api/hello.js +0 -23
- package/cli/builder/microService/skeletons/src/n-api/package.json +0 -15
- package/cli/builder/microService/skeletons/src/node/examples/index.js +0 -120
- package/cli/builder/microService/skeletons/src/node/index.js +0 -102
- package/cli/builder/microService/skeletons/src/node/services/markdown/markdown.js +0 -27
- package/cli/builder/microService/skeletons/src/node/services/servers/http.js +0 -45
- package/cli/builder/microService/skeletons/src/node/services/servers/https.js +0 -44
- package/cli/builder/microService/skeletons/src/node/services/socketio/socketio.js +0 -46
- package/cli/builder/microService/skeletons/src/node/services/syscall/syscall.js +0 -111
- package/cli/builder/microService/skeletons/src/node/services/worker/thread.js +0 -59
- package/cli/builder/microService/skeletons/src/node/services/worker/worker.js +0 -60
- package/cli/builder/microService/skeletons/src/templates/base.html +0 -12
- package/cli/builder/microService/skeletons/src/templates/socket.html +0 -23
- package/cli/builder/microService/skeletons/tests/unit/serviceTest.js +0 -30
- package/cli/builder/project/project.js +0 -474
- package/cli/builder/project/skeletons/README.md +0 -778
- package/cli/builder/project/skeletons/bin/dev-deploy.sh +0 -5
- package/cli/builder/project/skeletons/bin/generateCertificates.sh.skeleton +0 -180
- package/cli/builder/project/skeletons/bin/prod-deploy.sh +0 -3
- package/cli/builder/project/skeletons/config/config.js.skeleton +0 -178
- package/cli/builder/project/skeletons/config/openssl/ca/openssl.cnf.skeleton +0 -150
- package/cli/builder/project/skeletons/config/openssl/ca_intermediate/openssl.cnf.skeleton +0 -152
- package/cli/builder/project/skeletons/config/pm2.config.js.skeleton +0 -62
- package/cli/builder/project/skeletons/documentation.html.twig +0 -24
- package/cli/builder/project/skeletons/editorconfig.skeleton +0 -18
- package/cli/builder/project/skeletons/eslintignore.skeleton +0 -15
- package/cli/builder/project/skeletons/eslintrc.js.skeleton +0 -97
- package/cli/builder/project/skeletons/gitignore.skeleton +0 -32
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.37.entity-user.js +0 -137
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.38.entity-session.js +0 -97
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.39.entity-requests.js +0 -115
- package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.40.entity-jwts.js +0 -97
- package/cli/builder/project/skeletons/migrations/migrations.skeleton.js +0 -101
- package/cli/builder/project/skeletons/package.json.twig +0 -71
- package/cli/builder/react/reactBuilder.js +0 -224
- package/cli/builder/react/skeletons/app/Resources/databases/nodefony.db +0 -0
- package/cli/builder/react/skeletons/app/Resources/translations/messages.en_en.yml +0 -5
- package/cli/builder/react/skeletons/app/Resources/translations/messages.fr_fr.yml +0 -5
- package/cli/builder/react/skeletons/app/Resources/views/base.html.twig +0 -33
- package/cli/builder/react/skeletons/app/config/config.js +0 -392
- package/cli/builder/react/skeletons/app/config/routing.js +0 -10
- package/cli/builder/react/skeletons/app/config/security.js +0 -74
- package/cli/builder/react/skeletons/app/controller/appController.js +0 -22
- package/cli/builder/sandbox/sandBoxBuilder.js +0 -610
- package/cli/builder/sandbox/skeletons/Resources/css/entry.css +0 -48
- package/cli/builder/sandbox/skeletons/Resources/js/entry.js +0 -60
- package/cli/builder/sandbox/skeletons/Resources/public/manifest.json +0 -32
- package/cli/builder/sandbox/skeletons/Resources/translations/messages.en_en.yml +0 -5
- package/cli/builder/sandbox/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
- package/cli/builder/sandbox/skeletons/Resources/views/base.html.twig +0 -46
- package/cli/builder/sandbox/skeletons/Resources/views/index.html.twig +0 -34
- package/cli/builder/sandbox/skeletons/bootstrap/custom.scss +0 -27
- package/cli/builder/sandbox/skeletons/bootstrap/entry.scss +0 -60
- package/cli/builder/sandbox/skeletons/workbox/templates/index.html.twig +0 -32
- package/cli/builder/sandbox/skeletons/workbox/workers/service-worker.js +0 -196
- package/cli/builder/skeletons/Resources/public/favicon.ico +0 -0
- package/cli/builder/skeletons/Resources/public/images/app-logo.png +0 -0
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.json.twig +0 -24
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.json.twig +0 -20
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.json.twig +0 -20
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/500.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/base.html.twig +0 -30
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.json.twig +0 -24
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/footer.html.twig +0 -33
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/header.html.twig +0 -6
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.html.twig +0 -6
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.json.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.html.twig +0 -125
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.json.twig +0 -5
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.html.twig +0 -1
- package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.json.twig +0 -24
- package/cli/builder/skeletons/app/appKernel.js +0 -133
- package/cli/builder/skeletons/binding/binding.cc +0 -26
- package/cli/builder/skeletons/binding/binding.skeleton +0 -8
- package/cli/builder/skeletons/bundle/bundleClass.js +0 -37
- package/cli/builder/skeletons/command/commandClass.js +0 -11
- package/cli/builder/skeletons/command/taskClass.js +0 -20
- package/cli/builder/skeletons/config/config.js +0 -138
- package/cli/builder/skeletons/config/nodefony/elastic-bundle.js +0 -37
- package/cli/builder/skeletons/config/nodefony/framework-bundle.js +0 -22
- package/cli/builder/skeletons/config/nodefony/http-bundle.js +0 -104
- package/cli/builder/skeletons/config/nodefony/mail-bundle.js +0 -44
- package/cli/builder/skeletons/config/nodefony/mongoose-bundle.js +0 -51
- package/cli/builder/skeletons/config/nodefony/monitoring-bundle.js +0 -34
- package/cli/builder/skeletons/config/nodefony/realtime-bundle.js +0 -15
- package/cli/builder/skeletons/config/nodefony/redis-bundle.js +0 -44
- package/cli/builder/skeletons/config/nodefony/security-bundle.js +0 -25
- package/cli/builder/skeletons/config/nodefony/sequelize-bundle.js +0 -156
- package/cli/builder/skeletons/config/routing.js +0 -55
- package/cli/builder/skeletons/config/security.js +0 -102
- package/cli/builder/skeletons/config/services.js +0 -1
- package/cli/builder/skeletons/config/webpack/webpack.dev.config.js +0 -13
- package/cli/builder/skeletons/config/webpack/webpack.prod.config.js +0 -20
- package/cli/builder/skeletons/config/webpack.config.js +0 -148
- package/cli/builder/skeletons/controller/controllerClass.js +0 -75
- package/cli/builder/skeletons/package.json.twig +0 -57
- package/cli/builder/skeletons/unittest/testFile.js +0 -46
- package/cli/builder/vue/skeletons/Resources/public/images/app-logo.png +0 -0
- package/cli/builder/vue/skeletons/Resources/translations/messages.en_en.yml +0 -5
- package/cli/builder/vue/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
- package/cli/builder/vue/skeletons/vue.config.js +0 -129
- package/cli/builder/vue/vueBuilder.js +0 -410
- package/cli/generate/generate.es6 +0 -103
- package/cli/install/install.js +0 -190
- package/cli/sequelize/sequelize.js +0 -94
- package/cli/start.js +0 -702
- package/cli/tools/pm2.es6 +0 -112
- package/cli/tools/tools.es6 +0 -97
- package/cli.es6 +0 -925
- package/container.es6 +0 -272
- package/error.es6 +0 -293
- package/fileClass.es6 +0 -242
- package/finder/file.es6 +0 -33
- package/finder/fileResult.es6 +0 -138
- package/finder/finder2.es6 +0 -275
- package/finder.es6 +0 -427
- package/kernel/annotations/annotations.es6 +0 -286
- package/kernel/api/api.es6 +0 -61
- package/kernel/api/graphqlApi.es6 +0 -107
- package/kernel/api/jsonApi.es6 +0 -177
- package/kernel/api/openApi.es6 +0 -139
- package/kernel/api/schemas/openApiSchema.js +0 -394
- package/kernel/babylon.es6 +0 -162
- package/kernel/bundle.es6 +0 -1200
- package/kernel/cliKernel.es6 +0 -869
- package/kernel/command.es6 +0 -78
- package/kernel/commands/nodefonyCommand.es6 +0 -247
- package/kernel/commands/tasks/bundlesTask.es6 +0 -59
- package/kernel/controllers/controller.es6 +0 -791
- package/kernel/fixture.es6 +0 -19
- package/kernel/injections/injections.es6 +0 -424
- package/kernel/kernel.es6 +0 -1960
- package/kernel/kernelWatcher.es6 +0 -387
- package/kernel/orm/entity.es6 +0 -59
- package/kernel/orm/orm.es6 +0 -139
- package/kernel/reader.es6 +0 -290
- package/kernel/security/encoder.es6 +0 -17
- package/kernel/security/factories/factory.es6 +0 -111
- package/kernel/security/factories/passeportFactory.es6 +0 -61
- package/kernel/security/provider.es6 +0 -58
- package/kernel/security/providers/chainProvider.es6 +0 -99
- package/kernel/security/providers/memoryProvider.es6 +0 -55
- package/kernel/security/providers/providerManager.es6 +0 -67
- package/kernel/security/providers/userEntityProvider.es6 +0 -50
- package/kernel/security/role.es6 +0 -12
- package/kernel/security/secureArea.es6 +0 -431
- package/kernel/security/tokens/token.es6 +0 -171
- package/kernel/security/user.es6 +0 -106
- package/kernel/serviceRealTime.es6 +0 -21
- package/kernel/services/connectionsService.es6 +0 -93
- package/kernel/services/cron/cronService.es6 +0 -123
- package/kernel/task.es6 +0 -80
- package/kernel/templates.es6 +0 -22
- package/kernel/templating/twig.es6 +0 -218
- package/kernel/tests/cliTest.js +0 -59
- package/kernel/tests/containerTest.js +0 -232
- package/kernel/tests/kernelTest.js +0 -39
- package/nodefony.es6 +0 -1084
- package/notificationsCenter.es6 +0 -118
- package/protocol.es6 +0 -86
- package/protocols/bayeux.es6 +0 -174
- package/protocols/jsonRpc/jsonrpc.es6 +0 -56
- package/result.es6 +0 -99
- package/service.es6 +0 -288
- package/syslog/pdu.es6 +0 -141
- package/syslog/syslog.es6 +0 -754
- package/watcher.es6 +0 -113
|
@@ -0,0 +1,2156 @@
|
|
|
1
|
+
import { findReservedEntity } from "./reservedEntities.js";
|
|
2
|
+
import { findProjectRoot } from "../projectRoot.js";
|
|
3
|
+
import { envFileOrder } from "../../runtime/loadEnv.js";
|
|
4
|
+
import { pointeursInstructions } from "../agentTargets.js";
|
|
5
|
+
import { resolveInfra } from "../../config/infra.js";
|
|
6
|
+
import { resolveModuleLayout } from "./moduleLayout.js";
|
|
7
|
+
import { COLUMN_CASES, ENTITY_DIALECTS, ENTITY_ID_KINDS, buildEntityCodegen, describeColumnTypes, parseEntityFields, parseEntityIndexes, toSnakeCase } from "./entityFields.js";
|
|
8
|
+
import { assertUserFieldsAreFillable, readUserContract, userContractFields, userTableName } from "./userContractSource.js";
|
|
9
|
+
import { SCAFFOLD_VERSIONS, pick } from "./versions.js";
|
|
10
|
+
import { formatScaffoldOutput } from "./format.js";
|
|
11
|
+
import { ScaffoldWriter } from "./writer.js";
|
|
12
|
+
import { CONTROLLER_KIND_CHOICES, getScaffoldSpec } from "./spec.js";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { Eta } from "eta";
|
|
18
|
+
//#region src/cli/scaffold/engine.ts
|
|
19
|
+
/**
|
|
20
|
+
* Options du moteur de gabarits, **écrites une seule fois** : sept rendus les
|
|
21
|
+
* instanciaient à l'identique, et une divergence entre deux d'entre eux serait
|
|
22
|
+
* passée inaperçue jusqu'au fichier produit.
|
|
23
|
+
*
|
|
24
|
+
* - `autoEscape: false` — on génère du CODE, pas du HTML : échapper les entités
|
|
25
|
+
* corromprait chaque fichier.
|
|
26
|
+
* - `useWith: false` + `varName: "it"` — accès strict par `it.`, aucune variable
|
|
27
|
+
* implicite.
|
|
28
|
+
* - `autoTrim: false` — **le défaut d'eta AVALE la ligne vide qui suit une
|
|
29
|
+
* balise en fin de ligne.** Un `# <%= it.appName %>` suivi d'une ligne vide et
|
|
30
|
+
* d'un paragraphe rendait le titre COLLÉ au paragraphe ; et en markdown, un
|
|
31
|
+
* paragraphe qui suit une liste sans ligne vide devient une continuation de la
|
|
32
|
+
* dernière puce. Le rendu n'était donc pas seulement mal formaté, il était mal
|
|
33
|
+
* STRUCTURÉ — et le formateur que l'application embarque le refusait. Ce que
|
|
34
|
+
* le gabarit écrit est ce qui sort.
|
|
35
|
+
*/
|
|
36
|
+
const ETA_OPTIONS = {
|
|
37
|
+
useWith: false,
|
|
38
|
+
varName: "it",
|
|
39
|
+
autoEscape: false,
|
|
40
|
+
autoTrim: false
|
|
41
|
+
};
|
|
42
|
+
/** Fichiers dont le nom rendu diffère du template (npm strip les dotfiles publiés). */
|
|
43
|
+
const RENAMES = {
|
|
44
|
+
gitignore: ".gitignore",
|
|
45
|
+
dockerignore: ".dockerignore",
|
|
46
|
+
prettierignore: ".prettierignore",
|
|
47
|
+
gitattributes: ".gitattributes",
|
|
48
|
+
"oxlintrc.json": ".oxlintrc.json",
|
|
49
|
+
"prettierrc.json": ".prettierrc.json",
|
|
50
|
+
"gitlab-ci.yml": ".gitlab-ci.yml",
|
|
51
|
+
env: ".env",
|
|
52
|
+
"env.local": ".env.local"
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Répertoires renommés au rendu — même raison que `RENAMES` (npm strip aussi
|
|
56
|
+
* les DOSSIERS pointés publiés : un `templates/…/.github/` n'arriverait jamais
|
|
57
|
+
* chez l'installeur). Seul le PREMIER segment du chemin relatif est mappé.
|
|
58
|
+
*/
|
|
59
|
+
const DIR_RENAMES = { github: ".github" };
|
|
60
|
+
/**
|
|
61
|
+
* Paramètres frontend par framework — type registerEntry, entry, nœud de
|
|
62
|
+
* montage, ET les dépendances npm (SOURCE UNIQUE : consommée par le
|
|
63
|
+
* `package.json.tpl` de `create app` ET par `create front` qui les ajoute au
|
|
64
|
+
* package.json d'une cible existante — une version ne vit qu'ici).
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ **Le framework front lui-même est une devDependency, et `deps` est vide.**
|
|
67
|
+
* Le test qui tranche est le même que pour la toolchain : *le paquet est-il
|
|
68
|
+
* importé au RUNTIME du serveur ?* Ici non — aucun fichier hors `frontend/`
|
|
69
|
+
* n'importe `react`/`vue`/`@angular/*` : Vite les inline dans le bundle
|
|
70
|
+
* navigateur, et c'est ce bundle que la production sert. Les déclarer en
|
|
71
|
+
* `dependencies` les faisait survivre au `npm prune --omit=dev` de l'image et
|
|
72
|
+
* embarquer un framework entier que rien ne charge (mesuré ailleurs : 26,6 Mo
|
|
73
|
+
* pour `vue` seul, y compris dans une app React).
|
|
74
|
+
*
|
|
75
|
+
* L'ordre du `Dockerfile` généré est ce qui rend la bascule sûre :
|
|
76
|
+
* `RUN npm run build && npm prune --omit=dev` — le build les a quand il en a
|
|
77
|
+
* besoin, le prune les retire ensuite. Toute recette qui élaguerait AVANT de
|
|
78
|
+
* construire casserait, et c'est vrai de n'importe quelle devDependency.
|
|
79
|
+
*
|
|
80
|
+
* `deps` reste dans le contrat : le jour où un rendu côté serveur importera
|
|
81
|
+
* vraiment le framework, c'est là que la déclaration devra revenir.
|
|
82
|
+
*/
|
|
83
|
+
const FRONTEND_PARAMS = {
|
|
84
|
+
react: {
|
|
85
|
+
type: "react19",
|
|
86
|
+
entry: "./frontend/src/main.tsx",
|
|
87
|
+
mountNode: "<div id=\"root\"></div>",
|
|
88
|
+
deps: {},
|
|
89
|
+
devDeps: pick("react", "react-dom", "vite", "@vitejs/plugin-react", "@types/react", "@types/react-dom")
|
|
90
|
+
},
|
|
91
|
+
vue: {
|
|
92
|
+
type: "vue3",
|
|
93
|
+
entry: "./frontend/src/main.ts",
|
|
94
|
+
mountNode: "<div id=\"app\"></div>",
|
|
95
|
+
deps: {},
|
|
96
|
+
devDeps: pick("vue", "vite", "@vitejs/plugin-vue")
|
|
97
|
+
},
|
|
98
|
+
angular: {
|
|
99
|
+
type: "angular",
|
|
100
|
+
entry: "./frontend/src/main.ts",
|
|
101
|
+
mountNode: "<app-root></app-root>",
|
|
102
|
+
deps: {},
|
|
103
|
+
devDeps: pick("@angular/core", "@angular/common", "@angular/platform-browser", "vite", "@analogjs/vite-plugin-angular", "@angular/build", "@angular/compiler-cli")
|
|
104
|
+
},
|
|
105
|
+
svelte: {
|
|
106
|
+
type: "svelte5",
|
|
107
|
+
entry: "./frontend/src/main.ts",
|
|
108
|
+
mountNode: "<div id=\"app\"></div>",
|
|
109
|
+
deps: {},
|
|
110
|
+
devDeps: pick("svelte", "vite", "@sveltejs/vite-plugin-svelte")
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Paramètres de la base SQL retenue au scaffold — SOURCE UNIQUE du service
|
|
115
|
+
* docker, du port publié et de l'URL de connexion, consommée par les TROIS
|
|
116
|
+
* gabarits qui en parlent (`compose.yaml`, `.env`, `README.md`).
|
|
117
|
+
*
|
|
118
|
+
* Pourquoi ici plutôt que recopié en eta : ces valeurs doivent coïncider EXACTEMENT
|
|
119
|
+
* — l'URL du `.env` joint le port que le compose publie, et le README montre la
|
|
120
|
+
* même. Trois littéraux séparés divergent au premier réglage, et l'écart ne se
|
|
121
|
+
* voit qu'à la connexion refusée.
|
|
122
|
+
*
|
|
123
|
+
* ⚠️ **MySQL est publié sur 3306, pas 3307.** Le port décalé n'existait que pour
|
|
124
|
+
* faire cohabiter MySQL et MariaDB dans un compose qui portait les deux ; une
|
|
125
|
+
* app qui retient UN dialecte n'a plus rien avec quoi cohabiter, et un port
|
|
126
|
+
* inattendu est une friction gratuite.
|
|
127
|
+
*/
|
|
128
|
+
const DATABASE_PARAMS = {
|
|
129
|
+
postgres: {
|
|
130
|
+
service: "postgres",
|
|
131
|
+
label: "PostgreSQL 16",
|
|
132
|
+
scheme: "postgres",
|
|
133
|
+
port: 5432,
|
|
134
|
+
image: "postgres:16-alpine"
|
|
135
|
+
},
|
|
136
|
+
mariadb: {
|
|
137
|
+
service: "mariadb",
|
|
138
|
+
label: "MariaDB 11.4",
|
|
139
|
+
scheme: "mysql",
|
|
140
|
+
port: 3306,
|
|
141
|
+
image: "mariadb:11.4"
|
|
142
|
+
},
|
|
143
|
+
mysql: {
|
|
144
|
+
service: "mysql",
|
|
145
|
+
label: "MySQL 8.4",
|
|
146
|
+
scheme: "mysql",
|
|
147
|
+
port: 3306,
|
|
148
|
+
image: "mysql:8.4"
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Compose le bloc `db` offert aux gabarits — `null` pour sqlite, qui n'a ni
|
|
153
|
+
* service ni URL (le fichier vit dans `var/databases/`, l'ORM s'en charge seul).
|
|
154
|
+
*/
|
|
155
|
+
function resolveDatabase(choice, appName) {
|
|
156
|
+
if (choice === "sqlite") return null;
|
|
157
|
+
const params = DATABASE_PARAMS[choice];
|
|
158
|
+
const dsn = (database) => `${params.scheme}://${appName}:${appName}-dev@127.0.0.1:${params.port}/${database}`;
|
|
159
|
+
return {
|
|
160
|
+
choice,
|
|
161
|
+
...params,
|
|
162
|
+
database: appName,
|
|
163
|
+
databaseE2e: `${appName}_e2e`,
|
|
164
|
+
databaseScratch: `${appName}_e2e_scratch`,
|
|
165
|
+
url: dsn(appName),
|
|
166
|
+
urlE2e: dsn(`${appName}_e2e`),
|
|
167
|
+
urlScratch: dsn(`${appName}_e2e_scratch`)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Racine du paquet `nodefony` contenant `templates/` — remontée depuis CE fichier
|
|
172
|
+
* (source `src/cli/scaffold/`, bundle `dist/node/cli/scaffold/` : la remontée
|
|
173
|
+
* marche pour les deux sans dépendre de la profondeur).
|
|
174
|
+
*/
|
|
175
|
+
function findPackageRoot() {
|
|
176
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
177
|
+
for (let i = 0; i < 7; i++) {
|
|
178
|
+
const manifest = path.join(dir, "package.json");
|
|
179
|
+
if (existsSync(manifest)) try {
|
|
180
|
+
if (JSON.parse(readFileSync(manifest, "utf8")).name === "nodefony") return dir;
|
|
181
|
+
} catch {}
|
|
182
|
+
const parent = path.dirname(dir);
|
|
183
|
+
if (parent === dir) break;
|
|
184
|
+
dir = parent;
|
|
185
|
+
}
|
|
186
|
+
throw new Error("paquet nodefony introuvable (templates/)");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Workspaces nodefony d'un CHECKOUT du repo (`src/nodefony` + `src/packages/@nodefony/*`),
|
|
190
|
+
* résolus depuis la racine du paquet `nodefony`. Un paquet INSTALLÉ (node_modules)
|
|
191
|
+
* n'a pas ce voisinage → `null` (le mode link n'a de sens que sur un checkout).
|
|
192
|
+
*/
|
|
193
|
+
function resolveLocalWorkspaces(packageRoot) {
|
|
194
|
+
const packagesDir = path.resolve(packageRoot, "..", "packages", "@nodefony");
|
|
195
|
+
if (!existsSync(packagesDir)) return null;
|
|
196
|
+
const map = { nodefony: packageRoot };
|
|
197
|
+
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) if (entry.isDirectory()) map[`@nodefony/${entry.name}`] = path.join(packagesDir, entry.name);
|
|
198
|
+
return map;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Mode link : réécrit dans le `package.json` GÉNÉRÉ toute dep `nodefony` /
|
|
202
|
+
* `@nodefony/*` en `file:<workspace>` — `npm install` symlinke le checkout local
|
|
203
|
+
* et installe ses deps transitives. Rend l'app contrôlable AVANT toute release
|
|
204
|
+
* npm (dev du framework) ; les deps publiques (zod, react…) restent au registre.
|
|
205
|
+
*
|
|
206
|
+
* @returns les noms de paquets liés (triés, pour le récap)
|
|
207
|
+
* @throws si une dep du scope nodefony n'existe pas dans le checkout
|
|
208
|
+
*/
|
|
209
|
+
function linkLocalDeps(destDir, workspaces, writer) {
|
|
210
|
+
const manifestPath = path.join(destDir, "package.json");
|
|
211
|
+
const manifest = JSON.parse(writer.read(manifestPath));
|
|
212
|
+
const linked = [];
|
|
213
|
+
for (const block of ["dependencies", "devDependencies"]) {
|
|
214
|
+
const deps = manifest[block];
|
|
215
|
+
if (!deps) continue;
|
|
216
|
+
for (const name of Object.keys(deps)) {
|
|
217
|
+
if (name !== "nodefony" && !name.startsWith("@nodefony/")) continue;
|
|
218
|
+
const workspace = workspaces[name];
|
|
219
|
+
if (!workspace) throw new Error(`link : workspace introuvable pour ${name}`);
|
|
220
|
+
deps[name] = `file:${workspace.split(path.sep).join("/")}`;
|
|
221
|
+
linked.push(name);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
225
|
+
return linked.sort();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Complète et VALIDE des réponses partielles contre la spec d'un type : défauts
|
|
229
|
+
* appliqués, patterns vérifiés, choix contraints à leur liste. Le contexte des
|
|
230
|
+
* templates est donc complet PAR CONSTRUCTION (eta ne rend jamais un `undefined`
|
|
231
|
+
* silencieux). Les questions `askIf` non satisfaites sont forcées à `false` ;
|
|
232
|
+
* celles dont l'`askWhen` n'est pas satisfait retombent à leur DÉFAUT — une
|
|
233
|
+
* réponse qui porte sur ce qui ne sera pas généré n'a rien à décider.
|
|
234
|
+
*
|
|
235
|
+
* L'ordre de la spec fait foi : une question `askWhen` lit la réponse DÉJÀ
|
|
236
|
+
* résolue de celle qu'elle désigne, donc cette dernière doit la précéder.
|
|
237
|
+
*
|
|
238
|
+
* @throws Error si une valeur viole la spec (message = libellé + attendu)
|
|
239
|
+
*/
|
|
240
|
+
function resolveAnswers(spec, partial, caps) {
|
|
241
|
+
const answers = {};
|
|
242
|
+
for (const q of spec.questions) {
|
|
243
|
+
let value = partial[q.key];
|
|
244
|
+
if (q.askIf === "hasCheckout" && !caps.hasCheckout) value = false;
|
|
245
|
+
if (q.askWhen && String(answers[q.askWhen.key]) !== q.askWhen.equals) value = void 0;
|
|
246
|
+
if (value === void 0) value = q.default;
|
|
247
|
+
if (q.type === "boolean") {
|
|
248
|
+
answers[q.key] = value === true || value === "true";
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (q.type === "list") {
|
|
252
|
+
answers[q.key] = Array.isArray(value) ? value.map(String).filter((v) => v.length > 0) : typeof value === "string" && value.length > 0 ? [value] : [];
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const str = String(value);
|
|
256
|
+
if (q.type === "choice") {
|
|
257
|
+
if (!q.choices?.some((c) => c.value === str)) {
|
|
258
|
+
const allowed = q.choices?.map((c) => c.value).join(" | ") ?? "";
|
|
259
|
+
throw new Error(`${q.key} invalide « ${str} » — attendu : ${allowed}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (q.pattern && !new RegExp(q.pattern, "u").test(str)) throw new Error(`${q.key} invalide « ${str} » — ${q.patternHint ?? q.pattern}`);
|
|
263
|
+
answers[q.key] = str;
|
|
264
|
+
}
|
|
265
|
+
return answers;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Rend un layer de templates (`*.tpl`) dans `destDir` via eta. Les noms de
|
|
269
|
+
* fichiers peuvent porter des TOKENS (`__NAME__Controller.ts.tpl`) remplacés
|
|
270
|
+
* par `tokens` — un template ne peut pas mettre de tag eta dans un nom de
|
|
271
|
+
* fichier, le token est l'équivalent côté chemin.
|
|
272
|
+
*/
|
|
273
|
+
function renderLayer(eta, srcDir, destDir, data, written, writer, tokens) {
|
|
274
|
+
const entries = readdirSync(srcDir, {
|
|
275
|
+
withFileTypes: true,
|
|
276
|
+
recursive: true
|
|
277
|
+
});
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (!entry.isFile() || !entry.name.endsWith(".tpl")) continue;
|
|
280
|
+
const abs = path.join(entry.parentPath, entry.name);
|
|
281
|
+
let relDir = path.relative(srcDir, entry.parentPath);
|
|
282
|
+
const segments = relDir.split(path.sep);
|
|
283
|
+
const mapped = segments[0] !== void 0 && DIR_RENAMES[segments[0]];
|
|
284
|
+
if (mapped) relDir = path.join(mapped, ...segments.slice(1));
|
|
285
|
+
const base = entry.name.replace(/\.tpl$/u, "");
|
|
286
|
+
let rel = path.join(relDir, RENAMES[base] ?? base);
|
|
287
|
+
for (const [token, value] of Object.entries(tokens ?? {})) rel = rel.replaceAll(token, value);
|
|
288
|
+
const rendered = eta.renderString(readFileSync(abs, "utf8"), data);
|
|
289
|
+
if (rendered.includes("<%")) throw new Error(`tag eta résiduel dans ${rel}`);
|
|
290
|
+
if (rendered.trim() === "") continue;
|
|
291
|
+
writer.write(path.join(destDir, rel), rendered);
|
|
292
|
+
written.push(rel);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** Marqueurs de la zone préservée d'`AGENTS.md` — le contrat du merge borné. */
|
|
296
|
+
const APP_NOTES_START = "<!-- app-notes:start -->";
|
|
297
|
+
const APP_NOTES_END = "<!-- app-notes:end -->";
|
|
298
|
+
/**
|
|
299
|
+
* Réinjecte la zone `app-notes` du contenu précédent dans le rendu neuf.
|
|
300
|
+
*
|
|
301
|
+
* C'est TOUT le merge que le scaffold sait faire, et c'est voulu : `AGENTS.md`
|
|
302
|
+
* est 100 % dérivé (réécrit en entier à chaque régénération — il ne peut pas
|
|
303
|
+
* mentir), SAUF cette zone, où l'humain et l'agent accumulent les leçons
|
|
304
|
+
* propres à l'app. Un marqueur absent ou inversé (fichier retravaillé à la
|
|
305
|
+
* main) → le rendu neuf part tel quel : mieux vaut perdre des notes déplacées
|
|
306
|
+
* qu'écrire un fichier recousu de façon imprévisible.
|
|
307
|
+
*/
|
|
308
|
+
function preserveAppNotes(previous, next) {
|
|
309
|
+
const start = previous.indexOf(APP_NOTES_START);
|
|
310
|
+
const end = previous.indexOf(APP_NOTES_END);
|
|
311
|
+
if (start === -1 || end === -1 || end < start) return next;
|
|
312
|
+
const notes = previous.slice(start + 24, end);
|
|
313
|
+
const ns = next.indexOf(APP_NOTES_START);
|
|
314
|
+
const ne = next.indexOf(APP_NOTES_END);
|
|
315
|
+
if (ns === -1 || ne === -1 || ne < ns) return next;
|
|
316
|
+
return next.slice(0, ns + 24) + notes + next.slice(ne);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Rend `AGENTS.md` (+ le pointeur `CLAUDE.md`) à la racine du projet.
|
|
320
|
+
*
|
|
321
|
+
* Appelé par `create app` ET re-appelé par les scaffolds in-project qui
|
|
322
|
+
* changent l'inventaire décrit (`create module`) : régénération BORNÉE —
|
|
323
|
+
* réécriture complète depuis l'état RÉEL du projet, seule la zone `app-notes`
|
|
324
|
+
* est réinjectée (cf {@link preserveAppNotes}). Le moteur ne sait pas rejouer
|
|
325
|
+
* des templates sur de l'existant, et n'essaie pas : seuls ces fichiers
|
|
326
|
+
* 100 % dérivés sont régénérables.
|
|
327
|
+
*
|
|
328
|
+
* `CLAUDE.md` n'est écrit QUE s'il n'existe pas : c'est un pointeur d'une
|
|
329
|
+
* ligne, et un `CLAUDE.md` remplacé par l'utilisateur lui appartient.
|
|
330
|
+
*/
|
|
331
|
+
function renderProjectAgents(eta, packageRoot, projectRoot, data, written, writer) {
|
|
332
|
+
const tplDir = path.join(packageRoot, "templates", "app", "agents");
|
|
333
|
+
const agentsPath = path.join(projectRoot, "AGENTS.md");
|
|
334
|
+
let rendered = eta.renderString(readFileSync(path.join(tplDir, "AGENTS.md.tpl"), "utf8"), data);
|
|
335
|
+
if (rendered.includes("<%")) throw new Error("tag eta résiduel dans AGENTS.md");
|
|
336
|
+
if (writer.exists(agentsPath)) rendered = preserveAppNotes(writer.read(agentsPath), rendered);
|
|
337
|
+
writer.write(agentsPath, rendered);
|
|
338
|
+
written.push("AGENTS.md");
|
|
339
|
+
const template = readFileSync(path.join(tplDir, "POINTEUR.md.tpl"), "utf8");
|
|
340
|
+
for (const { file, agents } of pointeursInstructions(data.agents)) {
|
|
341
|
+
const target = path.join(projectRoot, file);
|
|
342
|
+
if (writer.exists(target)) continue;
|
|
343
|
+
const pointer = eta.renderString(template, {
|
|
344
|
+
...data,
|
|
345
|
+
pointeur: file,
|
|
346
|
+
agents: agents.join(" et ")
|
|
347
|
+
});
|
|
348
|
+
writer.write(target, pointer);
|
|
349
|
+
written.push(file);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Pose le fichier-POINTEUR d'instructions des agents donnés, s'il manque.
|
|
354
|
+
*
|
|
355
|
+
* Extrait de {@link renderProjectAgents} pour être appelable AILLEURS : c'est
|
|
356
|
+
* `nodefony ai:mcp --agent <clé>` qui câble un agent après la création de
|
|
357
|
+
* l'application, et c'est donc là que son pointeur doit apparaître. Sans cette
|
|
358
|
+
* fonction, filtrer les pointeurs à la création (pour ne plus déposer un
|
|
359
|
+
* `GEMINI.md` chez qui a demandé Claude) aurait privé de son fichier tout agent
|
|
360
|
+
* ajouté ensuite — on aurait remplacé un défaut par un autre.
|
|
361
|
+
*
|
|
362
|
+
* Un pointeur DÉJÀ présent n'est jamais réécrit : celui qu'un développeur a
|
|
363
|
+
* remplacé lui appartient.
|
|
364
|
+
*
|
|
365
|
+
* @param projectRoot - racine de l'application.
|
|
366
|
+
* @param keys - clés des agents concernés.
|
|
367
|
+
* @param appName - nom de l'application (rendu dans le gabarit).
|
|
368
|
+
* @returns les fichiers réellement écrits, relatifs à la racine.
|
|
369
|
+
*/
|
|
370
|
+
function writeAgentPointers(projectRoot, keys, appName) {
|
|
371
|
+
const pointeurs = pointeursInstructions(keys);
|
|
372
|
+
if (pointeurs.length === 0) return [];
|
|
373
|
+
const tplDir = path.join(findPackageRoot(), "templates", "app", "agents");
|
|
374
|
+
const template = readFileSync(path.join(tplDir, "POINTEUR.md.tpl"), "utf8");
|
|
375
|
+
const eta = new Eta({ autoEscape: false });
|
|
376
|
+
const written = [];
|
|
377
|
+
for (const { file, agents } of pointeurs) {
|
|
378
|
+
const target = path.join(projectRoot, file);
|
|
379
|
+
try {
|
|
380
|
+
writeFileSync(target, eta.renderString(template, {
|
|
381
|
+
appName,
|
|
382
|
+
pointeur: file,
|
|
383
|
+
agents: agents.join(" et ")
|
|
384
|
+
}), {
|
|
385
|
+
encoding: "utf8",
|
|
386
|
+
flag: "wx"
|
|
387
|
+
});
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (err.code === "EEXIST") continue;
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
written.push(file);
|
|
393
|
+
}
|
|
394
|
+
return written;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Cibles de scaffold du projet : l'app racine + les modules locaux
|
|
398
|
+
* (`modules/<x>/` portant `package.json` + `index.ts` — le layout produit par
|
|
399
|
+
* `create module`). Consommée par le CLI (validation `--module`) et par le
|
|
400
|
+
* futur formulaire Studio (liste de choix dynamique par projet).
|
|
401
|
+
*/
|
|
402
|
+
/**
|
|
403
|
+
* Capacités de l'environnement, telles que le MOTEUR les voit.
|
|
404
|
+
*
|
|
405
|
+
* Sert aux questions conditionnelles (`askIf`) : `link` (créer une app branchée sur le
|
|
406
|
+
* checkout local du framework, au lieu des paquets npm publiés) n'a de sens que si un
|
|
407
|
+
* checkout est résolvable. Un front ne peut PAS le deviner — seul le serveur sait ce
|
|
408
|
+
* qu'il y a sur le disque. Le figer côté client à `false` reviendrait à supprimer
|
|
409
|
+
* l'option en silence.
|
|
410
|
+
*
|
|
411
|
+
* @returns les capacités passées à {@link resolveAnswers}.
|
|
412
|
+
*/
|
|
413
|
+
function scaffoldCaps() {
|
|
414
|
+
return { hasCheckout: resolveLocalWorkspaces(findPackageRoot()) !== null };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Lit le contexte du projet contenant `dir`.
|
|
418
|
+
*
|
|
419
|
+
* @param dir - un dossier quelconque du projet (la racine est retrouvée seule).
|
|
420
|
+
* @param writer - accès fichiers (injectable pour les tests).
|
|
421
|
+
* @returns le contexte, ou `null` si `dir` n'est pas dans un projet Nodefony.
|
|
422
|
+
*/
|
|
423
|
+
function getScaffoldContext(dir, writer = new ScaffoldWriter()) {
|
|
424
|
+
const projectRoot = findProjectRoot(dir);
|
|
425
|
+
if (!projectRoot) return null;
|
|
426
|
+
const targets = listTargets(projectRoot, writer);
|
|
427
|
+
const entities = {};
|
|
428
|
+
for (const target of targets) entities[target.name] = readEntities(target.dir, writer);
|
|
429
|
+
return {
|
|
430
|
+
targets,
|
|
431
|
+
connectors: readConnectors(projectRoot, writer),
|
|
432
|
+
columnTypes: describeColumnTypes(),
|
|
433
|
+
entities,
|
|
434
|
+
idKinds: ENTITY_ID_KINDS
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function listTargets(projectRoot, writer = new ScaffoldWriter()) {
|
|
438
|
+
const readName = (dir) => {
|
|
439
|
+
try {
|
|
440
|
+
return JSON.parse(writer.read(path.join(dir, "package.json"))).name ?? null;
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
let rootManifest = {};
|
|
446
|
+
try {
|
|
447
|
+
rootManifest = JSON.parse(writer.read(path.join(projectRoot, "package.json")));
|
|
448
|
+
} catch {}
|
|
449
|
+
const targets = [{
|
|
450
|
+
kind: "app",
|
|
451
|
+
name: rootManifest.name ?? path.basename(projectRoot),
|
|
452
|
+
dir: projectRoot
|
|
453
|
+
}];
|
|
454
|
+
const layout = resolveModuleLayout(rootManifest, path.basename(projectRoot));
|
|
455
|
+
for (const rel of layout.targetDirs) {
|
|
456
|
+
const modulesDir = path.join(projectRoot, ...rel.split("/"));
|
|
457
|
+
if (!writer.exists(modulesDir)) continue;
|
|
458
|
+
for (const entry of writer.listDir(modulesDir)) {
|
|
459
|
+
const dir = path.join(modulesDir, entry.name);
|
|
460
|
+
if (!entry.isDirectory || !writer.exists(path.join(dir, "index.ts"))) continue;
|
|
461
|
+
targets.push({
|
|
462
|
+
kind: "module",
|
|
463
|
+
name: readName(dir) ?? entry.name,
|
|
464
|
+
dir
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return targets;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Résout la cible d'un scaffold in-project : l'application, ou le module
|
|
472
|
+
* désigné par `--module`.
|
|
473
|
+
*
|
|
474
|
+
* Un module a DEUX identités — son **nom npm** (`@app/blog`, celui qui vit dans
|
|
475
|
+
* son manifeste et dans celui de l'app) et son **dossier** (`blog`, celui qu'on
|
|
476
|
+
* a tapé pour le créer). N'accepter que le premier faisait échouer
|
|
477
|
+
* `--module blog` juste après un `create module blog` : mesuré sur un agent
|
|
478
|
+
* tiers, trois tentatives perdues sur cette seule asymétrie, alors que le nom
|
|
479
|
+
* court est celui que l'utilisateur a en tête.
|
|
480
|
+
*
|
|
481
|
+
* Le nom court est donc accepté **quand il désigne UNE cible**. Ambigu, il est
|
|
482
|
+
* refusé en nommant les candidats plutôt qu'en en choisissant un : un dépôt à
|
|
483
|
+
* plusieurs dossiers de workspaces peut porter `packages/@x/auth` ET
|
|
484
|
+
* `modules/auth` — deux modules distincts, même nom court. Le nom npm exact
|
|
485
|
+
* l'emporte toujours, et reste la façon de lever l'ambiguïté.
|
|
486
|
+
*
|
|
487
|
+
* @param projectRoot - racine de l'application (porte `nodefony.config.ts`).
|
|
488
|
+
* @param moduleName - valeur de `--module` ; vide = l'application elle-même.
|
|
489
|
+
* @param writer - accès disque injecté (simulation `--dry-run` comprise).
|
|
490
|
+
* @returns la cible résolue.
|
|
491
|
+
* @throws si le nom ne désigne aucune cible, ou s'il en désigne plusieurs.
|
|
492
|
+
*/
|
|
493
|
+
function resolveScaffoldTarget(projectRoot, moduleName, writer) {
|
|
494
|
+
const targets = listTargets(projectRoot, writer);
|
|
495
|
+
const app = targets[0];
|
|
496
|
+
if (!moduleName) return app;
|
|
497
|
+
const modules = targets.filter((t) => t.kind === "module");
|
|
498
|
+
const exact = modules.find((t) => t.name === moduleName);
|
|
499
|
+
if (exact) return exact;
|
|
500
|
+
const short = modules.filter((t) => path.basename(t.dir) === moduleName);
|
|
501
|
+
if (short.length === 1) return short[0];
|
|
502
|
+
if (short.length > 1) throw new Error(`« ${moduleName} » désigne ${short.length} modules — reprends le nom complet : ${short.map((t) => t.name).join(" · ")}`);
|
|
503
|
+
const known = targets.map((t) => `${t.name} (${t.kind})`).join(" · ");
|
|
504
|
+
throw new Error(`module « ${moduleName} » introuvable — cibles du projet : ${known}`);
|
|
505
|
+
}
|
|
506
|
+
/** `blog-post` / `BlogPost` / `blogPost` → `BlogPost`. */
|
|
507
|
+
function toPascalCase(name) {
|
|
508
|
+
return name.split(/[-_\s]+/u).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
509
|
+
}
|
|
510
|
+
/** `BlogPost` / `blog_post` → `blog-post`. */
|
|
511
|
+
function toKebabCase(name) {
|
|
512
|
+
return name.replace(/([a-z0-9])([A-Z])/gu, "$1-$2").replaceAll("_", "-").toLowerCase();
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Câble une classe générée dans le décorateur-liste de l'`index.ts` cible
|
|
516
|
+
* (`@controllers([...])`, `@entities([...])`, `@services([...])`) : ajoute
|
|
517
|
+
* l'import après le dernier import existant + insère le nom dans le tableau du
|
|
518
|
+
* décorateur. Édition TEXTUELLE gardée — toute ambiguïté = throw actionnable,
|
|
519
|
+
* jamais un fichier corrompu (le fichier n'est écrit que si les insertions
|
|
520
|
+
* tiennent).
|
|
521
|
+
*
|
|
522
|
+
* `controllers`/`entities` sont TOUJOURS rendus par les scaffolds qui les
|
|
523
|
+
* consomment (`@controllers([])` inconditionnel dans `app`/`module` ; les
|
|
524
|
+
* entités passent par {@link wireEntitiesDecorator}, qui crée déjà sa propre
|
|
525
|
+
* liste) — le décorateur introuvable y reste un refus. `services` diffère : le
|
|
526
|
+
* layer `module/base` ne le rend que si la question « service » a répondu
|
|
527
|
+
* `true`, et la racine d'une app n'en déclare JAMAIS un — le cas nominal d'un
|
|
528
|
+
* `create service` est donc une cible qui n'a PAS encore de `@services([...])`.
|
|
529
|
+
* Pour ce seul décorateur, l'absence n'est pas un refus : on le CRÉE, juste
|
|
530
|
+
* au-dessus de la classe (même position que le gabarit quand il le rend lui-même),
|
|
531
|
+
* en import ant `services` depuis `"nodefony"` s'il ne l'est pas déjà.
|
|
532
|
+
*
|
|
533
|
+
* @throws si la classe y est déjà, si aucun import n'ancre l'insertion, ou —
|
|
534
|
+
* pour `controllers`/`entities` seulement — si le décorateur est introuvable
|
|
535
|
+
* (le message donne l'édition manuelle exacte).
|
|
536
|
+
*/
|
|
537
|
+
/**
|
|
538
|
+
* L'expression d'un décorateur-liste RÉEL — jamais sa mention dans un commentaire.
|
|
539
|
+
*
|
|
540
|
+
* `String.replace` sans `g` réécrit la PREMIÈRE occurrence trouvée. Un `index.ts`
|
|
541
|
+
* dont le TSDoc cite `@services([…])` au-dessus de la classe — ce que fait le
|
|
542
|
+
* gabarit d'application, pour expliquer à quoi sert la liste — voyait donc sa
|
|
543
|
+
* PROSE réécrite : `@services([…, DiscountService]) est ce qui fait EXISTER un
|
|
544
|
+
* service`, phrase absurde qui s'allongeait à chaque service créé, pendant que
|
|
545
|
+
* le vrai décorateur, lui, était bien étendu (constaté sur un run du banc).
|
|
546
|
+
*
|
|
547
|
+
* L'ancre `^[ \t]*@` suffit à trancher : une ligne de TSDoc commence par ` * `.
|
|
548
|
+
* Le premier groupe capture l'indentation pour la rendre telle quelle — un
|
|
549
|
+
* décorateur de classe imbriquée n'a pas à être désaligné par sa réécriture.
|
|
550
|
+
*
|
|
551
|
+
* @param decorator - le décorateur-liste visé.
|
|
552
|
+
* @returns l'expression, multi-lignes, avec l'indentation en groupe 1 et le
|
|
553
|
+
* contenu de la liste en groupe 2.
|
|
554
|
+
*/
|
|
555
|
+
function decoratorListRe(decorator) {
|
|
556
|
+
return new RegExp(`^([ \\t]*)@${decorator}\\(\\[([^\\]]*)\\]\\)`, "mu");
|
|
557
|
+
}
|
|
558
|
+
function wireDecoratorList(indexPath, decorator, className, importPath, writer) {
|
|
559
|
+
const source = writer.read(indexPath);
|
|
560
|
+
if (new RegExp(`\\b${className}\\b`, "u").test(source)) throw new Error(`${className} est déjà référencé dans ${indexPath} — choisis un autre nom`);
|
|
561
|
+
const importLine = `import ${className} from "${importPath}";`;
|
|
562
|
+
const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
|
|
563
|
+
if (!last || last.index === void 0) throw new Error(`aucun import trouvé dans ${indexPath} — ajoute à la main :\n ${importLine}\n @${decorator}([..., ${className}])`);
|
|
564
|
+
const importAt = last.index + last[0].length;
|
|
565
|
+
const extraImport = decorator === "services" && !/\bservices\b[^\n]*from "nodefony"/u.test(source) ? `\nimport { services } from "nodefony";` : "";
|
|
566
|
+
const withImport = source.slice(0, importAt) + `\n${importLine}${extraImport}` + source.slice(importAt);
|
|
567
|
+
const decoRe = decoratorListRe(decorator);
|
|
568
|
+
const match = decoRe.exec(withImport);
|
|
569
|
+
if (match && match.index !== void 0) {
|
|
570
|
+
const list = match[2].trim();
|
|
571
|
+
const wired = withImport.replace(decoRe, `$1@${decorator}([${list ? `${list.replace(/,\s*$/u, "")}, ` : ""}${className}])`);
|
|
572
|
+
writer.write(indexPath, wired);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (decorator !== "services") throw new Error(`@${decorator}([...]) introuvable dans ${indexPath} — ajoute à la main :\n ${importLine}\n @${decorator}([${className}]) sur la classe Module`);
|
|
576
|
+
const classMatch = /^(?:export\s+(?:default\s+)?)?class\s+\w+\s+extends\s+Module\b/mu.exec(withImport);
|
|
577
|
+
if (!classMatch || classMatch.index === void 0) throw new Error(`« class … extends Module » introuvable dans ${indexPath} — ajoute à la main :\n ${importLine}\n @services([${className}]) juste au-dessus de la classe`);
|
|
578
|
+
const wired = withImport.slice(0, classMatch.index) + `@services([${className}])\n` + withImport.slice(classMatch.index);
|
|
579
|
+
writer.write(indexPath, wired);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Exécute un scaffold — point d'entrée UNIQUE des trois fronts (CLI rapide,
|
|
583
|
+
* CLI interactif, Studio), dispatché par type :
|
|
584
|
+
* - `app` : projet NEUF dans `request.dir` (layers base/preset/frontend) ;
|
|
585
|
+
* - `controller` : IN-PROJECT — `request.dir` est le point de départ de la
|
|
586
|
+
* détection de racine (cwd de l'appelant), la cible réelle vient de
|
|
587
|
+
* `answers.module` (vide = app racine).
|
|
588
|
+
*
|
|
589
|
+
* Toutes les écritures passent par une TRANSACTION ({@link ScaffoldWriter}) que
|
|
590
|
+
* seul le scaffold RACINE vide sur disque, une fois toutes les étapes passées :
|
|
591
|
+
* un refus, même tardif, ne laisse donc jamais de projet à moitié modifié. Et
|
|
592
|
+
* comme la simulation n'est que « la même exécution sans vidage », `dryRun`
|
|
593
|
+
* décrit exactement ce qui se passerait — pas ce qu'un second code de
|
|
594
|
+
* prévision croirait.
|
|
595
|
+
*
|
|
596
|
+
* @returns fichiers écrits + paquets liés — l'appelant décide du rendu (CLI ou JSON)
|
|
597
|
+
* @throws Error si réponses invalides, dossier non vide sans force, ou template cassé
|
|
598
|
+
*/
|
|
599
|
+
function runScaffold(request, version, options = {}) {
|
|
600
|
+
const writer = options.writer ?? new ScaffoldWriter();
|
|
601
|
+
const result = dispatchScaffold(request, version, writer);
|
|
602
|
+
if (options.writer) return result;
|
|
603
|
+
const forme = formatScaffoldOutput(writer, result.dest);
|
|
604
|
+
const rendered = forme.pending > 0 ? {
|
|
605
|
+
...result,
|
|
606
|
+
unformatted: forme.pending
|
|
607
|
+
} : result;
|
|
608
|
+
if (options.dryRun) return {
|
|
609
|
+
...rendered,
|
|
610
|
+
changes: writer.changes()
|
|
611
|
+
};
|
|
612
|
+
writer.commit();
|
|
613
|
+
return rendered;
|
|
614
|
+
}
|
|
615
|
+
/** Résout la spec puis route vers le scaffold du type demandé. */
|
|
616
|
+
function dispatchScaffold(request, version, writer) {
|
|
617
|
+
const [spec] = getScaffoldSpec(request.type);
|
|
618
|
+
if (!spec) throw new Error(`type de scaffold inconnu : ${request.type}`);
|
|
619
|
+
const packageRoot = findPackageRoot();
|
|
620
|
+
const workspaces = resolveLocalWorkspaces(packageRoot);
|
|
621
|
+
const answers = resolveAnswers(spec, request.answers, { hasCheckout: workspaces !== null });
|
|
622
|
+
if (request.type === "module") return runModuleScaffold(request, answers, packageRoot, version, writer);
|
|
623
|
+
if (request.type === "controller") return runControllerScaffold(request, answers, packageRoot, writer);
|
|
624
|
+
if (request.type === "service") return runServiceScaffold(request, answers, packageRoot, writer);
|
|
625
|
+
if (request.type === "front") return runFrontScaffold(request, answers, packageRoot, writer);
|
|
626
|
+
if (request.type === "entity") return runEntityScaffold(request, answers, packageRoot, writer);
|
|
627
|
+
if (request.type === "command") return runCommandScaffold(request, answers, packageRoot, writer);
|
|
628
|
+
const dest = path.resolve(request.dir);
|
|
629
|
+
if (existsSync(dest) && readdirSync(dest).length > 0 && !request.force) throw new Error(`le dossier ${dest} existe et n'est pas vide (--force pour écraser)`);
|
|
630
|
+
const preset = answers.preset;
|
|
631
|
+
const frontend = answers.frontend;
|
|
632
|
+
const front = frontend !== "none" ? FRONTEND_PARAMS[frontend] : null;
|
|
633
|
+
const db = resolveDatabase(answers.database, String(answers.name));
|
|
634
|
+
const data = {
|
|
635
|
+
appName: answers.name,
|
|
636
|
+
dialect: answers.database === "postgres" ? "postgres" : answers.database === "sqlite" || !answers.database ? "sqlite" : "mysql",
|
|
637
|
+
pascal: toPascalCase(String(answers.name)),
|
|
638
|
+
entryName: answers.name,
|
|
639
|
+
nodefonyVersion: version,
|
|
640
|
+
pkg: SCAFFOLD_VERSIONS,
|
|
641
|
+
preset,
|
|
642
|
+
complete: preset === "complete",
|
|
643
|
+
hasSecurity: preset === "complete",
|
|
644
|
+
hasOrm: preset === "complete",
|
|
645
|
+
frontend,
|
|
646
|
+
front,
|
|
647
|
+
db,
|
|
648
|
+
secrets: {
|
|
649
|
+
NF_TOTP_KEY: randomBytes(32).toString("base64"),
|
|
650
|
+
NF_WEBHOOK_KEY: randomBytes(32).toString("base64"),
|
|
651
|
+
NF_CSRF_SECRET: randomBytes(32).toString("base64")
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
655
|
+
const templates = path.join(packageRoot, "templates", request.type);
|
|
656
|
+
const written = [];
|
|
657
|
+
renderLayer(eta, path.join(templates, "base"), dest, data, written, writer);
|
|
658
|
+
if (!front) renderLayer(eta, path.join(templates, "home"), dest, data, written, writer);
|
|
659
|
+
renderLayer(eta, path.join(packageRoot, "templates", "controller", "hello"), dest, {
|
|
660
|
+
nameClass: "HelloController",
|
|
661
|
+
kebab: "hello",
|
|
662
|
+
route: "/api",
|
|
663
|
+
indexPath: "/hello",
|
|
664
|
+
helloName: answers.name,
|
|
665
|
+
secureRoute: preset === "complete"
|
|
666
|
+
}, written, writer, { __NAME__: "HelloController" });
|
|
667
|
+
if (preset === "complete") {
|
|
668
|
+
renderLayer(eta, path.join(templates, "complete"), dest, data, written, writer);
|
|
669
|
+
renderLayer(eta, path.join(packageRoot, "templates", "controller", "realtime"), dest, {
|
|
670
|
+
nameClass: "LiveController",
|
|
671
|
+
kebab: "live",
|
|
672
|
+
route: "/api/live",
|
|
673
|
+
channel: "live",
|
|
674
|
+
hasSecurity: true
|
|
675
|
+
}, written, writer, {
|
|
676
|
+
__NAME__: "LiveController",
|
|
677
|
+
__KEBAB__: "live"
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (front) {
|
|
681
|
+
renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-shell"), dest, data, written, writer);
|
|
682
|
+
renderLayer(eta, path.join(templates, "frontend", "shared"), dest, data, written, writer);
|
|
683
|
+
renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-entry", frontend), dest, data, written, writer);
|
|
684
|
+
renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-registrar"), dest, data, written, writer, { __PASCAL__: toPascalCase(String(answers.name)) });
|
|
685
|
+
renderLayer(eta, path.join(templates, "frontend", frontend), dest, data, written, writer);
|
|
686
|
+
if (frontend === "angular") renderLayer(eta, path.join(packageRoot, "templates", "shared", "ng-app-tsconfig"), dest, data, written, writer);
|
|
687
|
+
if (frontend === "vue") renderLayer(eta, path.join(packageRoot, "templates", "shared", "vue-shim"), dest, data, written, writer);
|
|
688
|
+
if (frontend === "svelte") renderLayer(eta, path.join(packageRoot, "templates", "shared", "svelte-shim"), dest, data, written, writer);
|
|
689
|
+
}
|
|
690
|
+
const chosenAgents = Array.isArray(answers.agents) ? answers.agents : [];
|
|
691
|
+
renderProjectAgents(eta, packageRoot, dest, {
|
|
692
|
+
appName: String(answers.name),
|
|
693
|
+
nodefonyVersion: version,
|
|
694
|
+
hasSecurity: preset === "complete",
|
|
695
|
+
hasOrm: preset === "complete",
|
|
696
|
+
hasRealtime: preset === "complete",
|
|
697
|
+
hasStudio: preset === "complete",
|
|
698
|
+
front: front !== null,
|
|
699
|
+
hasMigrateRecipe: writer.exists(path.join(dest, "deploy", "migrate-job.yaml")),
|
|
700
|
+
modules: [],
|
|
701
|
+
agents: chosenAgents
|
|
702
|
+
}, written, writer);
|
|
703
|
+
let linked = [];
|
|
704
|
+
if (answers.link === true) {
|
|
705
|
+
if (!workspaces) throw new Error("link exige un CHECKOUT de nodefony-core (paquet installé détecté) — sans checkout, attends la release npm puis installe sans --link");
|
|
706
|
+
linked = linkLocalDeps(dest, workspaces, writer);
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
dest,
|
|
710
|
+
files: written.sort(),
|
|
711
|
+
linked
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Déclare `modules/*` en workspaces npm de l'app et branche les scripts sur eux.
|
|
716
|
+
*
|
|
717
|
+
* POURQUOI c'est indispensable : le Kernel charge un module par son NOM
|
|
718
|
+
* (`import("@app/blog")`, cf `Kernel.loadModule`) — pas par un chemin. Sans le
|
|
719
|
+
* symlink que npm pose pour un workspace, le boot échoue sur « Cannot find
|
|
720
|
+
* package ». Les scripts sont chaînés dans la foulée, sinon le module ne serait
|
|
721
|
+
* ni construit (le bundler de l'app ne regarde que `index.ts` + `nodefony/**`),
|
|
722
|
+
* ni typé, ni testé : du code mort au premier jour.
|
|
723
|
+
*
|
|
724
|
+
* Idempotent — relancé pour le 2ᵉ module, il ne touche plus à rien.
|
|
725
|
+
*
|
|
726
|
+
* @returns true si le `package.json` de l'app a été modifié.
|
|
727
|
+
*/
|
|
728
|
+
function ensureWorkspaces(projectRoot, writer) {
|
|
729
|
+
const manifestPath = path.join(projectRoot, "package.json");
|
|
730
|
+
const manifest = JSON.parse(writer.read(manifestPath));
|
|
731
|
+
let changed = false;
|
|
732
|
+
const workspaces = manifest.workspaces ?? [];
|
|
733
|
+
if (!workspaces.includes("modules/*")) {
|
|
734
|
+
manifest.workspaces = [...workspaces, "modules/*"];
|
|
735
|
+
changed = true;
|
|
736
|
+
}
|
|
737
|
+
const scripts = manifest.scripts ??= {};
|
|
738
|
+
for (const [name, when] of Object.entries({
|
|
739
|
+
build: "before",
|
|
740
|
+
typecheck: "after",
|
|
741
|
+
test: "after"
|
|
742
|
+
})) {
|
|
743
|
+
const script = scripts[name];
|
|
744
|
+
if (!script || script.includes("--workspaces")) continue;
|
|
745
|
+
const delegated = `npm run ${name} --workspaces --if-present`;
|
|
746
|
+
scripts[name] = when === "before" ? `${delegated} && ${script}` : `${script} && ${delegated}`;
|
|
747
|
+
changed = true;
|
|
748
|
+
}
|
|
749
|
+
if (changed) writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
750
|
+
return changed;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Ajoute le module au manifeste `modules` de `nodefony.config.ts` — c'est CE
|
|
754
|
+
* tableau qui décide de ce qui se charge, et dans quel ordre. Un module local
|
|
755
|
+
* vient à la fin : le socle (http, framework, security…) doit être debout avant.
|
|
756
|
+
*
|
|
757
|
+
* Édition textuelle GARDÉE (même doctrine que `wireDecoratorList`) : on n'écrit
|
|
758
|
+
* que si l'ancre est trouvée sans ambiguïté, sinon on rend une note actionnable —
|
|
759
|
+
* jamais un `nodefony.config.ts` corrompu, qui empêcherait l'app de booter.
|
|
760
|
+
*
|
|
761
|
+
* @param moduleDir - dossier où le module a été créé, relatif à la racine (en
|
|
762
|
+
* `/`) — seulement pour NOMMER le bon endroit dans le commentaire inséré.
|
|
763
|
+
* @returns note à afficher si un geste manuel reste nécessaire, sinon `null`.
|
|
764
|
+
*/
|
|
765
|
+
function wireModuleManifest(configPath, pkgName, writer, moduleDir = "modules") {
|
|
766
|
+
const manual = `ajoute à la main dans le manifeste modules de nodefony.config.ts :\n use("${pkgName}", {}),`;
|
|
767
|
+
if (!writer.exists(configPath)) return manual;
|
|
768
|
+
const source = writer.read(configPath);
|
|
769
|
+
if (source.includes(`"${pkgName}"`)) return null;
|
|
770
|
+
const anchor = /modules\s*:\s*\[/u.exec(source);
|
|
771
|
+
if (!anchor || anchor.index === void 0) return manual;
|
|
772
|
+
let depth = 0;
|
|
773
|
+
let close = -1;
|
|
774
|
+
for (let i = anchor.index + anchor[0].length - 1; i < source.length; i++) {
|
|
775
|
+
const char = source[i];
|
|
776
|
+
if (char === "[") depth++;
|
|
777
|
+
else if (char === "]") {
|
|
778
|
+
depth--;
|
|
779
|
+
if (depth === 0) {
|
|
780
|
+
close = i;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (close === -1) return manual;
|
|
786
|
+
const entry = /import\s*\{[^}]*\buse\b[^}]*\}\s*from\s*"nodefony"/u.test(source) ? `use("${pkgName}", {}),` : `"${pkgName}",`;
|
|
787
|
+
const comment = `// Module local du projet (${moduleDir}/) — créé par \`nodefony create module\`.`;
|
|
788
|
+
writer.write(configPath, `${source.slice(0, close).trimEnd()}\n\n ${comment}\n ${entry}\n ${source.slice(close)}`);
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Déclare un rôle applicatif sous `ROLE_ADMIN` dans la hiérarchie de l'app.
|
|
793
|
+
*
|
|
794
|
+
* ## Pourquoi la hiérarchie, et pas une liste de rôles sur l'action
|
|
795
|
+
*
|
|
796
|
+
* Deux gestes rendent la même réponse sur la route qu'on vient d'écrire — une
|
|
797
|
+
* hiérarchie, ou `@IsGranted(["ROLE_X", "ROLE_ADMIN"])`. Un seul GÉNÉRALISE :
|
|
798
|
+
* la hiérarchie vaut pour toute route future gardée par ce rôle, la liste
|
|
799
|
+
* s'arrête à cette action-là et devra être recopiée à chaque fois. Le troisième
|
|
800
|
+
* geste — attribuer le rôle à l'administrateur au semis — est pire encore : il
|
|
801
|
+
* ne se voit dans aucun diff de route et se perd au premier compte recréé.
|
|
802
|
+
*
|
|
803
|
+
* ## Ce que cette fonction n'invente pas
|
|
804
|
+
*
|
|
805
|
+
* Elle n'ajoute JAMAIS une clé `roleHierarchy` absente, ni un `ROLE_ADMIN`
|
|
806
|
+
* absent : composer un bloc de configuration à l'aveugle dans un fichier que
|
|
807
|
+
* l'utilisateur a pu réorganiser produirait un manifeste qui ne boote plus.
|
|
808
|
+
* Quand l'ancre manque, elle rend la note à taper — même doctrine que
|
|
809
|
+
* {@link wireModuleManifest} et `wireDecoratorList`.
|
|
810
|
+
*
|
|
811
|
+
* @param configPath - `nodefony.config.ts` de l'application.
|
|
812
|
+
* @param role - le rôle à déclarer (déjà validé par le schéma de la question).
|
|
813
|
+
* @param writer - écrivain du scaffold (respecte `--dry-run`).
|
|
814
|
+
* @returns note à afficher si un geste manuel reste nécessaire, sinon `null`.
|
|
815
|
+
*/
|
|
816
|
+
function wireRoleHierarchy(configPath, role, writer) {
|
|
817
|
+
const manual = `ajoute à la main dans roleHierarchy de nodefony.config.ts :\n ROLE_ADMIN: ["ROLE_USER", "${role}"],\n (sans quoi un administrateur devra porter « ${role} » pour accéder à ce controller)`;
|
|
818
|
+
if (!writer.exists(configPath)) return manual;
|
|
819
|
+
const source = writer.read(configPath);
|
|
820
|
+
const block = /roleHierarchy\s*:\s*\{/u.exec(source);
|
|
821
|
+
if (!block || block.index === void 0) return manual;
|
|
822
|
+
let depth = 0;
|
|
823
|
+
let close = -1;
|
|
824
|
+
for (let i = block.index + block[0].length - 1; i < source.length; i++) {
|
|
825
|
+
const char = source[i];
|
|
826
|
+
if (char === "{") depth++;
|
|
827
|
+
else if (char === "}") {
|
|
828
|
+
depth--;
|
|
829
|
+
if (depth === 0) {
|
|
830
|
+
close = i;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (close === -1) return manual;
|
|
836
|
+
const body = source.slice(block.index, close);
|
|
837
|
+
if (body.includes(`"${role}"`)) return null;
|
|
838
|
+
const admin = /(ROLE_ADMIN\s*:\s*\[)([^\]]*)(\])/u.exec(body);
|
|
839
|
+
if (!admin || admin.index === void 0) return manual;
|
|
840
|
+
const inner = (admin[2] ?? "").replace(/,\s*$/u, "");
|
|
841
|
+
const separator = inner.trim().length > 0 ? ", " : "";
|
|
842
|
+
const replacement = `${admin[1]}${inner}${separator}"${role}"${admin[3]}`;
|
|
843
|
+
const patchedBody = body.slice(0, admin.index) + replacement + body.slice(admin.index + admin[0].length);
|
|
844
|
+
writer.write(configPath, source.slice(0, block.index) + patchedBody + source.slice(close));
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Noms des paquets que l'APPLICATION déclare (`dependencies` + `devDependencies`).
|
|
849
|
+
*
|
|
850
|
+
* Source unique des deux scaffolds qui doivent savoir si une brique du framework
|
|
851
|
+
* est installée : `create module` (refus AVANT écriture quand elle manque) et
|
|
852
|
+
* `create front` (qui pose la peer sur un module local quand l'app, elle, l'a).
|
|
853
|
+
*
|
|
854
|
+
* @param projectRoot - racine de l'application (porte `nodefony.config.ts`).
|
|
855
|
+
* @returns l'ensemble des noms déclarés — jamais les versions, seule la présence
|
|
856
|
+
* est une information ici.
|
|
857
|
+
*/
|
|
858
|
+
function readAppDependencyNames(projectRoot, writer) {
|
|
859
|
+
const manifest = JSON.parse(writer.read(path.join(projectRoot, "package.json")));
|
|
860
|
+
return /* @__PURE__ */ new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Scaffold IN-PROJECT d'un module applicatif : un WORKSPACE npm sous
|
|
864
|
+
* `modules/<nom>/` (package.json + build rolldown + config Zod + tests + docs),
|
|
865
|
+
* déclaré dans les workspaces de l'app et dans le manifeste `modules`.
|
|
866
|
+
*
|
|
867
|
+
* Ce scaffold pose la COQUILLE et rien d'autre : le controller et le frontend
|
|
868
|
+
* éventuels sont rendus par les scaffolds `controller` et `front` EXISTANTS,
|
|
869
|
+
* ciblés sur le module fraîchement créé. Aucun template n'est dupliqué — la
|
|
870
|
+
* commande qui sait poser un controller reste la seule à savoir le faire.
|
|
871
|
+
*
|
|
872
|
+
* @throws hors projet, module déjà présent (sans `--force`), nom en collision,
|
|
873
|
+
* ou brique manquante dans l'app (realtime/frontend) — avec le geste exact.
|
|
874
|
+
*/
|
|
875
|
+
function runModuleScaffold(request, answers, packageRoot, version, writer) {
|
|
876
|
+
const projectRoot = findProjectRoot(request.dir);
|
|
877
|
+
if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
|
|
878
|
+
const name = toKebabCase(String(answers.name));
|
|
879
|
+
const appManifestPath = path.join(projectRoot, "package.json");
|
|
880
|
+
const appManifest = JSON.parse(writer.read(appManifestPath));
|
|
881
|
+
const appName = appManifest.name ?? path.basename(projectRoot);
|
|
882
|
+
const layout = resolveModuleLayout(appManifest, path.basename(projectRoot));
|
|
883
|
+
const dest = path.join(projectRoot, ...layout.createDir.split("/"), name);
|
|
884
|
+
if (writer.exists(dest) && writer.listDir(dest).length > 0 && !request.force) throw new Error(`le module ${name} existe déjà (${layout.createDir}/${name}) — choisis un autre nom, ou --force`);
|
|
885
|
+
const pkgName = `${layout.scope}/${name}`;
|
|
886
|
+
const appDeps = readAppDependencyNames(projectRoot, writer);
|
|
887
|
+
const controller = String(answers.controller);
|
|
888
|
+
const frontend = answers.frontend;
|
|
889
|
+
if ((controller === "realtime" || controller === "duplex") && !appDeps.has("@nodefony/realtime")) throw new Error(`le controller ${controller} exige @nodefony/realtime, absent de l'app — ajoute la dep + use("@nodefony/realtime") au manifeste, ou --controller hello`);
|
|
890
|
+
if (frontend !== "none" && !appDeps.has("@nodefony/frontend")) throw new Error("un frontend exige @nodefony/frontend, absent de l'app — ajoute la dep + \"@nodefony/frontend\" au manifeste, ou --frontend none");
|
|
891
|
+
const pascal = toPascalCase(name);
|
|
892
|
+
const data = {
|
|
893
|
+
name,
|
|
894
|
+
pkgName,
|
|
895
|
+
appName,
|
|
896
|
+
pascal,
|
|
897
|
+
camel: pascal[0].toLowerCase() + pascal.slice(1),
|
|
898
|
+
upper: name.replaceAll("-", "_").toUpperCase(),
|
|
899
|
+
description: String(answers.description) || `Module ${name} de ${appName}`,
|
|
900
|
+
nodefonyVersion: version,
|
|
901
|
+
pkg: SCAFFOLD_VERSIONS,
|
|
902
|
+
service: answers.service === true,
|
|
903
|
+
command: answers.command === true,
|
|
904
|
+
controller,
|
|
905
|
+
needsRealtime: controller === "realtime" || controller === "duplex",
|
|
906
|
+
frontend,
|
|
907
|
+
front: frontend !== "none" ? FRONTEND_PARAMS[frontend] : null,
|
|
908
|
+
/**
|
|
909
|
+
* Le module naît-il paquet PUBLIABLE (surface npm : `exports`, `types`,
|
|
910
|
+
* `files`, peerDependencies, `.d.ts` générés) ou module local privé ?
|
|
911
|
+
* Décidé par le layout du dépôt, pas par une option — c'est une propriété du
|
|
912
|
+
* dossier où il atterrit.
|
|
913
|
+
*/
|
|
914
|
+
publishable: layout.kind === "packages",
|
|
915
|
+
/** Dossier où le module atterrit, relatif à la racine (en `/`, il VOYAGE). */
|
|
916
|
+
moduleDir: layout.createDir,
|
|
917
|
+
/**
|
|
918
|
+
* Version de départ. Un paquet d'un monorepo suit la version de SA racine
|
|
919
|
+
* (verrouillage naturel : tous les paquets du dépôt avancent ensemble) ; un
|
|
920
|
+
* module local d'application démarre à `0.1.0`, il a sa propre vie.
|
|
921
|
+
*/
|
|
922
|
+
version: layout.kind === "packages" ? appManifest.version ?? "0.1.0" : "0.1.0"
|
|
923
|
+
};
|
|
924
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
925
|
+
const templates = path.join(packageRoot, "templates", "module");
|
|
926
|
+
const tokens = {
|
|
927
|
+
__PASCAL__: pascal,
|
|
928
|
+
__KEBAB__: name
|
|
929
|
+
};
|
|
930
|
+
const written = [];
|
|
931
|
+
renderLayer(eta, path.join(templates, "base"), dest, data, written, writer, tokens);
|
|
932
|
+
if (data.service) renderLayer(eta, path.join(templates, "service"), dest, data, written, writer, tokens);
|
|
933
|
+
renderLayer(eta, path.join(templates, "ai"), dest, data, written, writer, tokens);
|
|
934
|
+
if (data.publishable) renderLayer(eta, path.join(templates, "packages"), dest, data, written, writer, tokens);
|
|
935
|
+
const notes = [];
|
|
936
|
+
if (data.command) {
|
|
937
|
+
const sub = runScaffold({
|
|
938
|
+
type: "command",
|
|
939
|
+
answers: {
|
|
940
|
+
name: "hello",
|
|
941
|
+
description: `Salue depuis le module ${name}`,
|
|
942
|
+
phase: "onReady",
|
|
943
|
+
module: pkgName,
|
|
944
|
+
service: data.service
|
|
945
|
+
},
|
|
946
|
+
dir: projectRoot,
|
|
947
|
+
force: request.force
|
|
948
|
+
}, version, { writer });
|
|
949
|
+
written.push(...sub.files);
|
|
950
|
+
notes.push(...sub.notes ?? []);
|
|
951
|
+
}
|
|
952
|
+
if (controller !== "none") {
|
|
953
|
+
const sub = runScaffold({
|
|
954
|
+
type: "controller",
|
|
955
|
+
answers: {
|
|
956
|
+
name,
|
|
957
|
+
kind: controller,
|
|
958
|
+
route: "",
|
|
959
|
+
module: pkgName
|
|
960
|
+
},
|
|
961
|
+
dir: projectRoot,
|
|
962
|
+
force: request.force
|
|
963
|
+
}, version, { writer });
|
|
964
|
+
written.push(...sub.files);
|
|
965
|
+
notes.push(...sub.notes ?? []);
|
|
966
|
+
}
|
|
967
|
+
if (frontend !== "none") {
|
|
968
|
+
const sub = runScaffold({
|
|
969
|
+
type: "front",
|
|
970
|
+
answers: {
|
|
971
|
+
name: controller === "none" ? name : `${name}-page`,
|
|
972
|
+
frontend,
|
|
973
|
+
route: `/${toKebabCase(name)}`,
|
|
974
|
+
module: pkgName
|
|
975
|
+
},
|
|
976
|
+
dir: projectRoot,
|
|
977
|
+
force: request.force
|
|
978
|
+
}, version, { writer });
|
|
979
|
+
written.push(...sub.files);
|
|
980
|
+
notes.push(...sub.notes ?? []);
|
|
981
|
+
}
|
|
982
|
+
if (!layout.workspaceDeclared && ensureWorkspaces(projectRoot, writer)) notes.push("package.json de l'app : workspaces modules/* + scripts build/typecheck/test chaînés");
|
|
983
|
+
const manifestNote = wireModuleManifest(path.join(projectRoot, "nodefony.config.ts"), pkgName, writer, layout.createDir);
|
|
984
|
+
notes.push(manifestNote ?? `nodefony.config.ts : use("${pkgName}", {}) ajouté au manifeste modules`);
|
|
985
|
+
if (!data.publishable) renderProjectAgents(eta, packageRoot, projectRoot, {
|
|
986
|
+
appName,
|
|
987
|
+
nodefonyVersion: version,
|
|
988
|
+
hasSecurity: appDeps.has("@nodefony/security"),
|
|
989
|
+
hasOrm: appDeps.has("@nodefony/orm-core"),
|
|
990
|
+
hasRealtime: appDeps.has("@nodefony/realtime"),
|
|
991
|
+
hasStudio: appDeps.has("@nodefony/studio"),
|
|
992
|
+
front: appDeps.has("@nodefony/frontend"),
|
|
993
|
+
hasMigrateRecipe: writer.exists(path.join(projectRoot, "deploy", "migrate-job.yaml")),
|
|
994
|
+
agents: [],
|
|
995
|
+
modules: listTargets(projectRoot, writer).filter((t) => t.kind === "module").map((t) => ({
|
|
996
|
+
name: t.name,
|
|
997
|
+
dir: path.relative(projectRoot, t.dir).split(path.sep).join("/")
|
|
998
|
+
}))
|
|
999
|
+
}, written, writer);
|
|
1000
|
+
return {
|
|
1001
|
+
dest,
|
|
1002
|
+
files: written.sort(),
|
|
1003
|
+
linked: [],
|
|
1004
|
+
notes
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Scaffold IN-PROJECT d'un controller : résout la cible (app racine ou module),
|
|
1009
|
+
* rend le template de la saveur (`hello`/`rest`/`duplex`/`realtime`/`example`)
|
|
1010
|
+
* dans `<cible>/nodefony/controllers/` puis câble la classe dans le
|
|
1011
|
+
* `@controllers([...])` de l'`index.ts` cible.
|
|
1012
|
+
*
|
|
1013
|
+
* @throws hors projet, cible inconnue (le message liste les cibles), saveur
|
|
1014
|
+
* realtime/duplex sans dep `@nodefony/realtime`, ou wiring impossible
|
|
1015
|
+
* (actionnable).
|
|
1016
|
+
*/
|
|
1017
|
+
function runControllerScaffold(request, answers, packageRoot, writer) {
|
|
1018
|
+
const projectRoot = findProjectRoot(request.dir);
|
|
1019
|
+
if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
|
|
1020
|
+
const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
|
|
1021
|
+
const kind = answers.kind;
|
|
1022
|
+
const base = String(answers.name).replace(/[-_]?[Cc]ontroller$/u, "");
|
|
1023
|
+
const nameClass = `${toPascalCase(base)}Controller`;
|
|
1024
|
+
const kebab = toKebabCase(base);
|
|
1025
|
+
const route = String(answers.route) || `/api/${kebab}`;
|
|
1026
|
+
const manifest = JSON.parse(writer.read(path.join(target.dir, "package.json")));
|
|
1027
|
+
const targetDeps = new Set([
|
|
1028
|
+
"dependencies",
|
|
1029
|
+
"devDependencies",
|
|
1030
|
+
"peerDependencies"
|
|
1031
|
+
].flatMap((b) => Object.keys(manifest[b] ?? {})));
|
|
1032
|
+
if (!CONTROLLER_KIND_CHOICES.includes(kind)) throw new Error(`saveur « ${String(kind)} » inconnue — choix : ${CONTROLLER_KIND_CHOICES.join(" | ")}`);
|
|
1033
|
+
if ((kind === "realtime" || kind === "duplex") && !targetDeps.has("@nodefony/realtime")) throw new Error(`la saveur ${kind} exige @nodefony/realtime dans ${target.name} — ajoute la dep + use("@nodefony/realtime") au manifeste, ou choisis --kind hello`);
|
|
1034
|
+
const role = String(answers.role ?? "").trim();
|
|
1035
|
+
if (role.length > 0 && !targetDeps.has("@nodefony/security")) throw new Error(`--role ${role} exige @nodefony/security dans ${target.name} — sans lui, @IsGranted n'est évalué par personne et la route resterait OUVERTE.\n → l'ajouter : npm install @nodefony/security, puis use("@nodefony/security", {}) au manifeste`);
|
|
1036
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
1037
|
+
const written = [];
|
|
1038
|
+
const data = {
|
|
1039
|
+
nameClass,
|
|
1040
|
+
kebab,
|
|
1041
|
+
route,
|
|
1042
|
+
channel: kebab,
|
|
1043
|
+
hasSecurity: targetDeps.has("@nodefony/security"),
|
|
1044
|
+
indexPath: "",
|
|
1045
|
+
helloName: kebab,
|
|
1046
|
+
secureRoute: false,
|
|
1047
|
+
roleGuard: role.length > 0,
|
|
1048
|
+
role
|
|
1049
|
+
};
|
|
1050
|
+
renderLayer(eta, path.join(packageRoot, "templates", "controller", kind), target.dir, data, written, writer, {
|
|
1051
|
+
__NAME__: nameClass,
|
|
1052
|
+
__KEBAB__: kebab
|
|
1053
|
+
});
|
|
1054
|
+
wireDecoratorList(path.join(target.dir, "index.ts"), "controllers", nameClass, `./nodefony/controllers/${nameClass}`, writer);
|
|
1055
|
+
written.push("index.ts");
|
|
1056
|
+
const noteRole = role.length > 0 ? wireRoleHierarchy(path.join(projectRoot, "nodefony.config.ts"), role, writer) : null;
|
|
1057
|
+
const NOTES = {
|
|
1058
|
+
hello: [`GET ${route}`, `WS ${route}/echo`],
|
|
1059
|
+
rest: [`REST ${route} (GET/POST) · ${route}/{id} (GET/PUT/PATCH/DELETE)`],
|
|
1060
|
+
duplex: [`REST ${route} (GET/POST) · ${route}/{id} (GET/DELETE)`, `WS ${route}/realtime (socket Nodefony — mêmes actions via socket.request/mutate)`],
|
|
1061
|
+
realtime: [`WS ${route}/realtime (socket Nodefony — canal ${kebab}:ticker, action ${kebab}:ping)`, `TEST tests/${kebab}-realtime.test.ts — \`npx vitest run\` (harnais @nodefony/realtime/testing, ni serveur ni navigateur)`],
|
|
1062
|
+
example: [`REST ${route} (vitrine décorateurs — voir les curl du TSDoc généré)`, `WS ${route}/echo`]
|
|
1063
|
+
};
|
|
1064
|
+
return {
|
|
1065
|
+
dest: target.dir,
|
|
1066
|
+
files: written.sort(),
|
|
1067
|
+
linked: [],
|
|
1068
|
+
notes: [...NOTES[kind], ...role.length > 0 ? [`GARDE ${role} sur TOUT le controller (@IsGranted de classe) — ` + (noteRole === null ? `déclaré sous ROLE_ADMIN dans roleHierarchy : l'administrateur y a accès sans porter ce rôle` : `⚠ hiérarchie NON modifiée — ${noteRole}`)] : []]
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Scaffold IN-PROJECT d'un service injectable : résout la cible (app racine ou
|
|
1073
|
+
* module), rend la classe (`@injectable`, `extends Service`) + son interface
|
|
1074
|
+
* dans `<cible>/nodefony/{service,interfaces}/`, puis câble la classe dans le
|
|
1075
|
+
* `@services([...])` de l'`index.ts` cible — CRÉÉ s'il n'existe pas encore
|
|
1076
|
+
* (cf {@link wireDecoratorList}, seul cas où l'absence du décorateur n'est pas
|
|
1077
|
+
* un refus).
|
|
1078
|
+
*
|
|
1079
|
+
* Volontairement AUTONOME : contrairement au service PRINCIPAL d'un module
|
|
1080
|
+
* (`create module`, gabarit `templates/module/service/`), celui-ci ne dépend
|
|
1081
|
+
* d'aucun `nodefony/config/config.ts` — une cible existante (app racine, ou
|
|
1082
|
+
* module créé avec `--no-service`) n'en a pas forcément un, et le résultat
|
|
1083
|
+
* doit compiler tel quel, sans édition manuelle.
|
|
1084
|
+
*
|
|
1085
|
+
* @throws hors projet, cible inconnue (le message liste les cibles), ou nom
|
|
1086
|
+
* déjà référencé dans l'`index.ts` cible.
|
|
1087
|
+
*/
|
|
1088
|
+
function runServiceScaffold(request, answers, packageRoot, writer) {
|
|
1089
|
+
const projectRoot = findProjectRoot(request.dir);
|
|
1090
|
+
if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
|
|
1091
|
+
const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
|
|
1092
|
+
const base = String(answers.name).replace(/[-_]?[Ss]ervice$/u, "");
|
|
1093
|
+
const pascal = toPascalCase(base);
|
|
1094
|
+
const nameClass = `${pascal}Service`;
|
|
1095
|
+
const camel = pascal[0].toLowerCase() + pascal.slice(1);
|
|
1096
|
+
const existing = listTargetServices(target.dir, writer);
|
|
1097
|
+
const injectName = String(answers.inject ?? "").trim();
|
|
1098
|
+
let inject = null;
|
|
1099
|
+
if (injectName !== "") {
|
|
1100
|
+
const wanted = `${toPascalCase(injectName.replace(/[-_]?[Ss]ervice$/u, ""))}Service`;
|
|
1101
|
+
const found = existing.find((s) => s.pascal === wanted);
|
|
1102
|
+
if (!found) {
|
|
1103
|
+
const known = existing.map((s) => s.pascal).join(" · ") || "aucun";
|
|
1104
|
+
throw new Error(`--inject : « ${injectName} » introuvable dans ${target.name} (services de la cible : ${known}) — crée-le d'abord, ou relance sans --inject`);
|
|
1105
|
+
}
|
|
1106
|
+
if (found.pascal === nameClass) throw new Error(`--inject : un service ne s'injecte pas lui-même (${nameClass})`);
|
|
1107
|
+
const method = findCallableMethod(found.source);
|
|
1108
|
+
if (method === null) throw new Error(`--inject : ${found.pascal} n'expose aucune méthode publique appelable sans argument obligatoire — le service injecté serait déclaré sans être appelé`);
|
|
1109
|
+
inject = {
|
|
1110
|
+
pascal: found.pascal,
|
|
1111
|
+
key: found.key,
|
|
1112
|
+
camel: found.key[0].toLowerCase() + found.key.slice(1),
|
|
1113
|
+
method
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
1117
|
+
const written = [];
|
|
1118
|
+
renderLayer(eta, path.join(packageRoot, "templates", "service"), target.dir, {
|
|
1119
|
+
pascal,
|
|
1120
|
+
camel,
|
|
1121
|
+
inject,
|
|
1122
|
+
description: String(answers.description) || `Service ${pascal} de ${target.name}`
|
|
1123
|
+
}, written, writer, { __PASCAL__: pascal });
|
|
1124
|
+
wireDecoratorList(path.join(target.dir, "index.ts"), "services", nameClass, `./nodefony/service/${nameClass}`, writer);
|
|
1125
|
+
written.push("index.ts");
|
|
1126
|
+
const notes = [`DI container.get("${camel}") — clé du conteneur (super("${camel}", …))`, `DI @inject("${nameClass}") — nom de classe (@injectable)`];
|
|
1127
|
+
if (inject) notes.push(`DI ${inject.pascal} est injecté par le CONSTRUCTEUR — dépendance déclarée, ordonnée par le conteneur`);
|
|
1128
|
+
else if (existing.length > 0) notes.push(`DI pour appeler ${existing[0].pascal} depuis ce service, injecte-le : nodefony create service ${base} --inject ${existing[0].pascal}`);
|
|
1129
|
+
return {
|
|
1130
|
+
dest: target.dir,
|
|
1131
|
+
files: written.sort(),
|
|
1132
|
+
linked: [],
|
|
1133
|
+
notes
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Retire commentaires de bloc et de ligne d'une source TypeScript.
|
|
1138
|
+
*
|
|
1139
|
+
* ⚠️ Approximation assumée — une chaîne contenant `//` (une URL) perd sa fin.
|
|
1140
|
+
* C'est sans effet ici : les lecteurs ci-dessous cherchent une FORME de code
|
|
1141
|
+
* (`super("…"`, une signature de méthode), jamais le contenu d'une chaîne.
|
|
1142
|
+
* L'inverse, lui, s'est produit : un TSDoc qui CITE `super("autreService", …)`
|
|
1143
|
+
* pour expliquer la clé d'un voisin faisait lire cette clé-là. Un fichier bien
|
|
1144
|
+
* documenté est le cas NORMAL, pas le cas tordu.
|
|
1145
|
+
*
|
|
1146
|
+
* @param source - le contenu du fichier.
|
|
1147
|
+
* @returns la même source, commentaires blanchis.
|
|
1148
|
+
*/
|
|
1149
|
+
function stripComments(source) {
|
|
1150
|
+
return source.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/[^\n]*/gu, "$1");
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Nom Nodefony déclaré par le premier `super("…", …)` d'un fichier — celui d'un
|
|
1154
|
+
* `Module` (`index.ts`) ou d'un `Service`.
|
|
1155
|
+
*
|
|
1156
|
+
* C'est la CLÉ du conteneur, et pour un module le préfixe de ses commandes CLI
|
|
1157
|
+
* (`<module>:<action>`). Elle ne se déduit ni du nom npm du paquet ni du nom de
|
|
1158
|
+
* la classe : les trois peuvent différer, et seule celle-ci existe au runtime.
|
|
1159
|
+
*
|
|
1160
|
+
* Lue sur le CODE seul : un `super("…")` cité dans un TSDoc pour documenter un
|
|
1161
|
+
* autre service faisait rendre la clé du voisin — la commande générée importait
|
|
1162
|
+
* alors une classe et résolvait l'instance d'une autre.
|
|
1163
|
+
*
|
|
1164
|
+
* @returns le nom déclaré, ou `null` si le fichier ne suit pas la forme attendue.
|
|
1165
|
+
*/
|
|
1166
|
+
function readNodefonyName(file, writer) {
|
|
1167
|
+
try {
|
|
1168
|
+
return /\bsuper\(\s*"([^"]+)"/u.exec(stripComments(writer.read(file)))?.[1] ?? null;
|
|
1169
|
+
} catch {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Première méthode d'un service qu'une commande peut appeler SANS argument.
|
|
1175
|
+
*
|
|
1176
|
+
* On cherche une méthode **publique** dont tous les paramètres sont facultatifs
|
|
1177
|
+
* (aucun, ou tous avec `?`/valeur par défaut) : c'est la seule forme dont on
|
|
1178
|
+
* puisse produire un appel qui compile à coup sûr. Le cycle de vie (`init`) et
|
|
1179
|
+
* le constructeur sont exclus — les appeler serait un contresens.
|
|
1180
|
+
*
|
|
1181
|
+
* ⚠️ Analyse TEXTUELLE, volontairement conservatrice : à la moindre signature
|
|
1182
|
+
* qu'on ne sait pas lire, on passe. Mieux vaut refuser `--service` que livrer un
|
|
1183
|
+
* appel qui ne compile pas.
|
|
1184
|
+
*
|
|
1185
|
+
* @returns le nom de la méthode, ou `null` si aucune ne convient.
|
|
1186
|
+
*/
|
|
1187
|
+
function findCallableMethod(source) {
|
|
1188
|
+
for (const m of source.matchAll(/^ {2}(?:public\s+)?(?:async\s+)?([a-z][A-Za-z0-9_]*)\s*\(([^)]*)\)/gmu)) {
|
|
1189
|
+
const [, name, params] = m;
|
|
1190
|
+
if (name === "constructor" || name === "init" || name === "if") continue;
|
|
1191
|
+
const args = params.trim();
|
|
1192
|
+
if (args === "" || args.split(",").every((p) => p.includes("=") || /\?\s*:/u.test(p) || p.trim() === "")) return name;
|
|
1193
|
+
}
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Les services d'une cible : classe, clé de conteneur, source.
|
|
1198
|
+
*
|
|
1199
|
+
* Point UNIQUE qui sait où vivent les services et comment on les nomme —
|
|
1200
|
+
* `--service` de `create command` et `--inject` de `create service` en
|
|
1201
|
+
* dépendent tous deux. Deux façons de « trouver les services » divergeraient en
|
|
1202
|
+
* silence, chacune passant ses propres essais.
|
|
1203
|
+
*
|
|
1204
|
+
* Un fichier dont on ne sait pas lire le `super("…", …)` est ignoré : sans clé
|
|
1205
|
+
* de conteneur, il n'est atteignable par personne, donc il n'est pas un
|
|
1206
|
+
* candidat.
|
|
1207
|
+
*/
|
|
1208
|
+
function listTargetServices(targetDir, writer) {
|
|
1209
|
+
const dir = path.join(targetDir, "nodefony", "service");
|
|
1210
|
+
if (!writer.exists(dir)) return [];
|
|
1211
|
+
const found = [];
|
|
1212
|
+
for (const entry of writer.listDir(dir)) {
|
|
1213
|
+
if (entry.isDirectory || !entry.name.endsWith("Service.ts")) continue;
|
|
1214
|
+
const file = path.join(dir, entry.name);
|
|
1215
|
+
const key = readNodefonyName(file, writer);
|
|
1216
|
+
if (key === null) continue;
|
|
1217
|
+
found.push({
|
|
1218
|
+
pascal: entry.name.replace(/\.ts$/u, ""),
|
|
1219
|
+
key,
|
|
1220
|
+
source: writer.read(file)
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
return found;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Service appelable de la cible : sa classe, sa clé de conteneur, sa méthode.
|
|
1227
|
+
*
|
|
1228
|
+
* ⚠️ La méthode est CHERCHÉE, pas supposée. Elle a longtemps été exigée par son
|
|
1229
|
+
* nom (`greet`, celui du gabarit) — or ce même gabarit dit « à remplacer par la
|
|
1230
|
+
* vôtre » : suivre le conseil cassait `--service`, sur un message qui réclamait
|
|
1231
|
+
* une méthode d'exemple. Un générateur ne doit pas exiger que son propre exemple
|
|
1232
|
+
* soit resté intact.
|
|
1233
|
+
*
|
|
1234
|
+
* ⚠️ **Le premier venu n'est une réponse que s'il est le SEUL.** Sans `wanted`,
|
|
1235
|
+
* cette fonction rendait le premier service de l'ordre du disque — donc le
|
|
1236
|
+
* premier alphabétiquement. Tant qu'une application n'avait qu'un service
|
|
1237
|
+
* d'exemple, le hasard tombait juste ; dès le deuxième, la commande générée
|
|
1238
|
+
* appelait un service que personne n'avait demandé, et rien ne le disait. Une
|
|
1239
|
+
* application réelle en a dix.
|
|
1240
|
+
*
|
|
1241
|
+
* @param targetDir - racine de la cible (app ou module).
|
|
1242
|
+
* @param writer - accès disque injecté.
|
|
1243
|
+
* @param wanted - nom du service voulu : classe (`ReportService`, `Report`) ou
|
|
1244
|
+
* clé de conteneur (`report`). Omis, la cible ne doit en avoir qu'un.
|
|
1245
|
+
* @returns `{ pascal, key, method }`, ou `null` si la cible n'a aucun service
|
|
1246
|
+
* dont une méthode soit appelable sans argument.
|
|
1247
|
+
* @throws Si `wanted` ne correspond à rien, ou si plusieurs services sont
|
|
1248
|
+
* appelables et qu'aucun n'est désigné — dans les deux cas en NOMMANT les
|
|
1249
|
+
* candidats : refuser en donnant le choix coûte un essai, produire un appel
|
|
1250
|
+
* vers le mauvais service coûte un débogage.
|
|
1251
|
+
*/
|
|
1252
|
+
function findTargetService(targetDir, writer, wanted) {
|
|
1253
|
+
const callables = [];
|
|
1254
|
+
for (const { pascal, key, source } of listTargetServices(targetDir, writer)) {
|
|
1255
|
+
const method = findCallableMethod(source);
|
|
1256
|
+
if (method !== null) callables.push({
|
|
1257
|
+
pascal,
|
|
1258
|
+
key,
|
|
1259
|
+
method
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
if (callables.length === 0) return null;
|
|
1263
|
+
const names = callables.map((s) => `${s.pascal} (clé « ${s.key} »)`).join(", ");
|
|
1264
|
+
if (wanted !== void 0) {
|
|
1265
|
+
const needle = wanted.toLowerCase().replace(/service$/u, "");
|
|
1266
|
+
const found = callables.find((s) => s.pascal.toLowerCase().replace(/service$/u, "") === needle || s.key.toLowerCase() === wanted.toLowerCase());
|
|
1267
|
+
if (found === void 0) throw new Error(`--service ${wanted} : aucun service de ce nom dans ${targetDir} — services appelables : ${names}`);
|
|
1268
|
+
return found;
|
|
1269
|
+
}
|
|
1270
|
+
if (callables.length > 1) throw new Error(`--service : ${callables.length} services appelables, précise lequel (--service <Nom>) — ${names}`);
|
|
1271
|
+
return callables[0];
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Câble `this.addCommand(<Classe>)` dans le constructeur du Module d'un
|
|
1275
|
+
* `index.ts` : ajoute l'import après le dernier import, insère l'appel juste
|
|
1276
|
+
* après le `super(…)` du constructeur.
|
|
1277
|
+
*
|
|
1278
|
+
* L'ancre est le `super(…)` et non l'accolade du constructeur : c'est la seule
|
|
1279
|
+
* ligne dont l'existence est GARANTIE (un `Module` doit appeler `super`), et
|
|
1280
|
+
* insérer après elle vaut pour un constructeur vide comme pour un constructeur
|
|
1281
|
+
* déjà rempli. Édition textuelle gardée, même contrat que
|
|
1282
|
+
* {@link wireDecoratorList} : toute ambiguïté = throw actionnable, jamais un
|
|
1283
|
+
* fichier corrompu.
|
|
1284
|
+
*
|
|
1285
|
+
* @throws si la classe y est déjà, si aucun import n'ancre l'insertion, ou si
|
|
1286
|
+
* le `super(…)` du constructeur est introuvable (le message donne l'édition
|
|
1287
|
+
* manuelle exacte).
|
|
1288
|
+
*/
|
|
1289
|
+
function wireCommandCall(indexPath, className, importPath, writer) {
|
|
1290
|
+
const source = writer.read(indexPath);
|
|
1291
|
+
const importLine = `import ${className} from "${importPath}";`;
|
|
1292
|
+
const callLine = `this.addCommand(${className});`;
|
|
1293
|
+
if (new RegExp(`\\b${className}\\b`, "u").test(source)) throw new Error(`${className} est déjà référencé dans ${indexPath} — choisis un autre nom`);
|
|
1294
|
+
const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
|
|
1295
|
+
if (!last || last.index === void 0) throw new Error(`aucun import trouvé dans ${indexPath} — ajoute à la main :\n ${importLine}\n ${callLine} dans le constructeur`);
|
|
1296
|
+
const importAt = last.index + last[0].length;
|
|
1297
|
+
const withImport = source.slice(0, importAt) + `\n${importLine}` + source.slice(importAt);
|
|
1298
|
+
const match = /super\([^()]*\);/u.exec(withImport);
|
|
1299
|
+
if (!match || match.index === void 0) throw new Error(`super(…) du constructeur introuvable dans ${indexPath} — ajoute à la main :\n ${importLine}\n ${callLine} après le super(…) du constructeur`);
|
|
1300
|
+
const lineStart = withImport.lastIndexOf("\n", match.index) + 1;
|
|
1301
|
+
const indent = /^[ \t]*/u.exec(withImport.slice(lineStart, match.index))?.[0] ?? " ";
|
|
1302
|
+
const at = match.index + match[0].length;
|
|
1303
|
+
writer.write(indexPath, `${withImport.slice(0, at)}\n${indent}${callLine}${withImport.slice(at)}`);
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Scaffold IN-PROJECT d'une commande CLI : rend la classe dans
|
|
1307
|
+
* `<cible>/nodefony/command/` puis la câble dans le constructeur du Module
|
|
1308
|
+
* cible (`this.addCommand(…)`).
|
|
1309
|
+
*
|
|
1310
|
+
* Le nom complet est DÉRIVÉ : `<module>:<action>`, où le module est celui que
|
|
1311
|
+
* l'`index.ts` de la cible déclare (`super("blog", …)`) — pas le nom npm du
|
|
1312
|
+
* paquet, qui peut différer. Écrire le préfixe soi-même est toléré (il est
|
|
1313
|
+
* strippé), jamais exigé.
|
|
1314
|
+
*
|
|
1315
|
+
* @throws hors projet, cible inconnue (le message liste les cibles), action
|
|
1316
|
+
* vide, phase inconnue, `--service` sans service appelable, ou wiring
|
|
1317
|
+
* impossible (actionnable).
|
|
1318
|
+
*/
|
|
1319
|
+
function runCommandScaffold(request, answers, packageRoot, writer) {
|
|
1320
|
+
const projectRoot = findProjectRoot(request.dir);
|
|
1321
|
+
if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
|
|
1322
|
+
const moduleName = String(answers.module ?? "");
|
|
1323
|
+
const target = resolveScaffoldTarget(projectRoot, moduleName, writer);
|
|
1324
|
+
const indexPath = path.join(target.dir, "index.ts");
|
|
1325
|
+
const prefix = readNodefonyName(indexPath, writer) ?? target.name.split("/").pop();
|
|
1326
|
+
let action = String(answers.name).trim().toLowerCase();
|
|
1327
|
+
if (action === prefix) action = "";
|
|
1328
|
+
else if (action.startsWith(`${prefix}:`)) action = action.slice(prefix.length + 1);
|
|
1329
|
+
if (action === "") throw new Error(`action requise (le préfixe « ${prefix} » est ajouté seul) — ex : nodefony create command publish${moduleName ? ` --module ${moduleName}` : ""}`);
|
|
1330
|
+
const phase = String(answers.phase);
|
|
1331
|
+
const commandName = `${prefix}:${action}`;
|
|
1332
|
+
const nameClass = `${toPascalCase(action.replaceAll(":", "-"))}Command`;
|
|
1333
|
+
const serviceName = String(answers.serviceName ?? "").trim();
|
|
1334
|
+
const service = answers.service === true ? findTargetService(target.dir, writer, serviceName === "" ? void 0 : serviceName) : null;
|
|
1335
|
+
if (answers.service === true && service === null) throw new Error(`--service : aucun service appelable dans ${target.name} (attendu : nodefony/service/*Service.ts exposant une méthode publique sans argument obligatoire) — relance sans --service`);
|
|
1336
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
1337
|
+
const written = [];
|
|
1338
|
+
renderLayer(eta, path.join(packageRoot, "templates", "command"), target.dir, {
|
|
1339
|
+
nameClass,
|
|
1340
|
+
commandName,
|
|
1341
|
+
phase,
|
|
1342
|
+
description: String(answers.description) || `Commande ${commandName} de ${prefix}`,
|
|
1343
|
+
service
|
|
1344
|
+
}, written, writer, { __NAME__: nameClass });
|
|
1345
|
+
wireCommandCall(indexPath, nameClass, `./nodefony/command/${nameClass}`, writer);
|
|
1346
|
+
written.push("index.ts");
|
|
1347
|
+
return {
|
|
1348
|
+
dest: target.dir,
|
|
1349
|
+
files: written.sort(),
|
|
1350
|
+
linked: [],
|
|
1351
|
+
notes: [`CLI nodefony ${commandName} [who] [-j] (phase ${phase})`, `visible dans \`nodefony --help\` une fois le module construit`]
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Valeur d'exemple d'un champ — VALIDE au regard du schéma de l'entité.
|
|
1356
|
+
*
|
|
1357
|
+
* Un échantillon n'est pas une décoration : les tests générés s'en servent pour
|
|
1358
|
+
* insérer, et la documentation pour montrer un appel qui marche. S'il viole le
|
|
1359
|
+
* schéma, l'entité naît avec un test rouge, et la première impression du
|
|
1360
|
+
* générateur est qu'il produit du code cassé.
|
|
1361
|
+
*
|
|
1362
|
+
* Ce piège s'est présenté trois fois, sur trois types ajoutés à trois moments
|
|
1363
|
+
* différents — l'énumération, puis le décimal et le caractère fixe. Chaque fois,
|
|
1364
|
+
* la valeur générique `nom-1` passait à côté d'une contrainte que le schéma, lui,
|
|
1365
|
+
* faisait respecter. D'où cette fonction unique : tout type qui restreint ses
|
|
1366
|
+
* valeurs doit dire ici ce qu'il accepte, sinon rien ne le rappellera.
|
|
1367
|
+
*
|
|
1368
|
+
* @param field - le champ à illustrer.
|
|
1369
|
+
* @param id - stratégie de clé primaire (une référence vers une clé numérique
|
|
1370
|
+
* attend un nombre, pas une chaîne).
|
|
1371
|
+
* @returns `fixed` pour le JSON de la documentation, `expr` pour la fabrique
|
|
1372
|
+
* paramétrée des tests (une expression TypeScript où `n` varie).
|
|
1373
|
+
*/
|
|
1374
|
+
/**
|
|
1375
|
+
* Le filtre sur lequel le test généré peut prouver qu'un filtre FILTRE — ou
|
|
1376
|
+
* `null` si aucun champ ne s'y prête.
|
|
1377
|
+
*
|
|
1378
|
+
* Prouver un refus (`?actf=x` → 400) et prouver un filtre sont deux choses
|
|
1379
|
+
* différentes, et la seconde exige une ligne qui NE matche PAS. Sans elle,
|
|
1380
|
+
* l'assertion « toutes les lignes rendues portent la valeur demandée » reste
|
|
1381
|
+
* vraie quand le filtre est ignoré : tous les échantillons d'un booléen valent
|
|
1382
|
+
* `true`, ceux d'une énumération valent sa première valeur ({@link sampleValue}).
|
|
1383
|
+
* Le test serait vert sur un filtre débranché — exactement le défaut qu'il est
|
|
1384
|
+
* censé attraper.
|
|
1385
|
+
*
|
|
1386
|
+
* D'où les deux seules natures retenues, celles qui offrent un contraire sûr et
|
|
1387
|
+
* gratuit. Une clé étrangère en est écartée : poser une seconde valeur
|
|
1388
|
+
* demanderait de créer l'entité visée, et le test parlerait d'autre chose.
|
|
1389
|
+
*
|
|
1390
|
+
* Deux écritures de chaque valeur, et ce n'est pas une redondance : `match` part
|
|
1391
|
+
* dans l'URL (`?publie=true` — tout y est du texte), `matchJson` est la
|
|
1392
|
+
* littérale TypeScript posée dans le corps envoyé (`publie: true`, un booléen —
|
|
1393
|
+
* `"true"` y serait refusé par le schéma).
|
|
1394
|
+
*
|
|
1395
|
+
* @param fields - les champs déclarés de l'entité.
|
|
1396
|
+
* @returns le nom du filtre et ses deux valeurs sous les deux écritures, ou
|
|
1397
|
+
* `null` si l'entité n'a aucun champ éprouvable.
|
|
1398
|
+
*/
|
|
1399
|
+
function filterProbe(fields) {
|
|
1400
|
+
for (const f of fields) {
|
|
1401
|
+
if (f.nullable) continue;
|
|
1402
|
+
if (f.type === "bool") return {
|
|
1403
|
+
name: f.name,
|
|
1404
|
+
match: "true",
|
|
1405
|
+
matchJson: "true",
|
|
1406
|
+
other: "false",
|
|
1407
|
+
otherJson: "false"
|
|
1408
|
+
};
|
|
1409
|
+
if (f.type === "enum" && (f.values?.length ?? 0) >= 2) {
|
|
1410
|
+
const [a, b] = f.values;
|
|
1411
|
+
return {
|
|
1412
|
+
name: f.name,
|
|
1413
|
+
match: a,
|
|
1414
|
+
matchJson: JSON.stringify(a),
|
|
1415
|
+
other: b,
|
|
1416
|
+
otherJson: JSON.stringify(b)
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Le filtre sur lequel une valeur MAL FORMÉE existe, et de quoi la fabriquer —
|
|
1424
|
+
* ou `null` si aucun filtre de l'entité ne peut en refuser une.
|
|
1425
|
+
*
|
|
1426
|
+
* Toutes les natures ne refusent pas : un filtre `"string"` accepte n'importe
|
|
1427
|
+
* quelle chaîne, par construction. Le test généré visait pourtant le PREMIER
|
|
1428
|
+
* filtre déclaré quel qu'il soit, et attendait un `400` — sur une entité dont
|
|
1429
|
+
* le seul filtre est une clé étrangère à identifiant textuel
|
|
1430
|
+
* (`author:ref:Author` avec des `uuid`), il exigeait donc le refus d'une valeur
|
|
1431
|
+
* parfaitement valide, et échouait sur le générateur au lieu de l'éprouver.
|
|
1432
|
+
*
|
|
1433
|
+
* @param filters - le vocabulaire de filtre déjà calculé (nom → littérale de sa
|
|
1434
|
+
* nature, telle qu'elle sera écrite dans le gabarit).
|
|
1435
|
+
* @returns le nom du filtre et une valeur qu'il doit refuser, ou `null`.
|
|
1436
|
+
*/
|
|
1437
|
+
function malformedProbe(filters) {
|
|
1438
|
+
for (const f of filters) {
|
|
1439
|
+
if (f.def === "\"boolean\"") return {
|
|
1440
|
+
name: f.name,
|
|
1441
|
+
value: "oui"
|
|
1442
|
+
};
|
|
1443
|
+
if (f.def === "\"int\"") return {
|
|
1444
|
+
name: f.name,
|
|
1445
|
+
value: "abc"
|
|
1446
|
+
};
|
|
1447
|
+
if (f.def.startsWith("[")) return {
|
|
1448
|
+
name: f.name,
|
|
1449
|
+
value: "valeur-hors-domaine"
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
function sampleValue(field, id) {
|
|
1455
|
+
const { name, type } = field;
|
|
1456
|
+
if (type === "int" || type === "float") return {
|
|
1457
|
+
fixed: 1,
|
|
1458
|
+
expr: "n"
|
|
1459
|
+
};
|
|
1460
|
+
if (type === "bool") return {
|
|
1461
|
+
fixed: true,
|
|
1462
|
+
expr: "true"
|
|
1463
|
+
};
|
|
1464
|
+
if (type === "json") return {
|
|
1465
|
+
fixed: {},
|
|
1466
|
+
expr: "{}"
|
|
1467
|
+
};
|
|
1468
|
+
if (type === "date") return {
|
|
1469
|
+
fixed: "2026-01-01T00:00:00.000Z",
|
|
1470
|
+
expr: "new Date()"
|
|
1471
|
+
};
|
|
1472
|
+
if (type === "enum") {
|
|
1473
|
+
const value = field.defaultValue ?? field.values?.[0] ?? "";
|
|
1474
|
+
return {
|
|
1475
|
+
fixed: value,
|
|
1476
|
+
expr: JSON.stringify(value)
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const echantillonUuid = () => ({
|
|
1480
|
+
fixed: `00000000-0000-4000-8000-${"1".padStart(12, "0")}`,
|
|
1481
|
+
expr: `\`00000000-0000-4000-8000-\${String(n).padStart(12, "0")}\``
|
|
1482
|
+
});
|
|
1483
|
+
if (type === "ref") return id === "serial" ? {
|
|
1484
|
+
fixed: 1,
|
|
1485
|
+
expr: "n"
|
|
1486
|
+
} : echantillonUuid();
|
|
1487
|
+
if (type === "uuid") return echantillonUuid();
|
|
1488
|
+
if (type === "decimal") return {
|
|
1489
|
+
fixed: "12.34",
|
|
1490
|
+
expr: "`${n}.50`"
|
|
1491
|
+
};
|
|
1492
|
+
if (type === "char") {
|
|
1493
|
+
const size = field.length ?? 1;
|
|
1494
|
+
return {
|
|
1495
|
+
fixed: "A".repeat(size),
|
|
1496
|
+
expr: `String(n).padStart(${size}, "A").slice(-${size})`
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
const max = field.length;
|
|
1500
|
+
if (type === "string" && max !== void 0) return {
|
|
1501
|
+
fixed: `${name}-1`.slice(0, max),
|
|
1502
|
+
expr: `\`${name}-\${n}\`.slice(0, ${max})`
|
|
1503
|
+
};
|
|
1504
|
+
return {
|
|
1505
|
+
fixed: `${name}-1`,
|
|
1506
|
+
expr: `\`${name}-\${n}\``
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
/** Pluriel simple, suffisant pour un nom de table (`Post` → `posts`, `Story` → `stories`). */
|
|
1510
|
+
function pluralize(word) {
|
|
1511
|
+
if (/[^aeiou]y$/u.test(word)) return `${word.slice(0, -1)}ies`;
|
|
1512
|
+
if (/(s|x|z|ch|sh)$/u.test(word)) return `${word}es`;
|
|
1513
|
+
return `${word}s`;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* `BlogPost` → `blog_posts` — nom de table SQL par DÉFAUT.
|
|
1517
|
+
*
|
|
1518
|
+
* Par défaut seulement : `--table` impose un nom littéral, seul moyen d'épouser une
|
|
1519
|
+
* table qui existe déjà (`website`, `session`) — la pluralisation ne se devine pas à
|
|
1520
|
+
* l'envers, et une table réelle ne demande la permission à personne.
|
|
1521
|
+
*
|
|
1522
|
+
* La casse vient de {@link toSnakeCase}, la même que celle des colonnes : deux
|
|
1523
|
+
* grammaires auraient fini par diverger sur un nom composé.
|
|
1524
|
+
*/
|
|
1525
|
+
function tableName(pascal) {
|
|
1526
|
+
return pluralize(toSnakeCase(pascal));
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Identifiant SQL acceptable — minuscules, chiffres, tiret bas.
|
|
1530
|
+
*
|
|
1531
|
+
* Volontairement plus strict que ce que les moteurs tolèrent entre guillemets : un
|
|
1532
|
+
* nom qui exige d'être cité voyage mal d'un dialecte à l'autre, et le générateur
|
|
1533
|
+
* l'écrirait sans les guillemets qui le sauvent.
|
|
1534
|
+
*/
|
|
1535
|
+
const SQL_NAME_RE = /^[a-z_][a-z0-9_]*$/u;
|
|
1536
|
+
/** Refuse un identifiant SQL AVANT toute écriture, en nommant ce qui est attendu. */
|
|
1537
|
+
function assertSqlName(value, option) {
|
|
1538
|
+
if (!SQL_NAME_RE.test(value)) throw new Error(`${option} invalide « ${value} » — un identifiant SQL commence par une lettre minuscule ou « _ », puis minuscules, chiffres et « _ » (ex : website_id)`);
|
|
1539
|
+
return value;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Dialecte SQL du connecteur visé — **l'infra déclarée d'abord**, le fichier ensuite.
|
|
1543
|
+
*
|
|
1544
|
+
* L'ordre n'est pas un détail de confort : c'est celui du RUNTIME. Une
|
|
1545
|
+
* application déclare sa base par URL (`NF_DATABASE_URL`, modèle « infra
|
|
1546
|
+
* déclarée » — docker, conteneur, CI, production), et `nodefony.config.ts` ne
|
|
1547
|
+
* porte alors aucun dialecte. Le scaffold, qui ne lisait que le fichier,
|
|
1548
|
+
* retombait sur `sqlite` et générait du Drizzle SQLite pour une application qui
|
|
1549
|
+
* tourne sur PostgreSQL. Le DDL de développement créait ensuite les colonnes
|
|
1550
|
+
* telles quelles : chaînes bornées devenues `text`, identifiants `uuid` devenus
|
|
1551
|
+
* `text` — et là c'est structurel, PostgreSQL refuse `text = uuid`, donc toute
|
|
1552
|
+
* jointure écrite ensuite échoue.
|
|
1553
|
+
*
|
|
1554
|
+
* Mesuré sur un schéma réel (banc de schéma, umami) : **18 identifiants
|
|
1555
|
+
* dégradés et 32 longueurs perdues** sur 83 colonnes, sans un mot.
|
|
1556
|
+
*
|
|
1557
|
+
* La déduction depuis l'URL n'est pas réécrite ici : {@link resolveInfra} en est
|
|
1558
|
+
* la source unique, la même que celle qu'exécute le kernel. Un scaffold qui
|
|
1559
|
+
* dériverait son propre scheme divergerait au premier alias ajouté.
|
|
1560
|
+
*
|
|
1561
|
+
* Le fichier reste consulté ensuite : un connecteur qui déclare explicitement
|
|
1562
|
+
* son dialecte l'emporte sur un défaut, et une app multi-connecteurs n'a pas
|
|
1563
|
+
* qu'une base.
|
|
1564
|
+
*/
|
|
1565
|
+
function detectDialect(projectRoot, writer, connector = "default") {
|
|
1566
|
+
const connectors = readConnectors(projectRoot, writer);
|
|
1567
|
+
const found = connectors.find((c) => c.name === connector);
|
|
1568
|
+
if (found && declaresDialect(projectRoot, writer, connector)) return found.dialect;
|
|
1569
|
+
const declared = infraDialect(projectRoot);
|
|
1570
|
+
if (declared) return declared;
|
|
1571
|
+
if (found) return found.dialect;
|
|
1572
|
+
return connectors[0]?.dialect ?? "sqlite";
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Le dialecte du connecteur est-il ÉCRIT dans `nodefony.config.ts` ?
|
|
1576
|
+
*
|
|
1577
|
+
* {@link readConnectors} ne peut pas répondre : il rend `sqlite` aussi bien pour
|
|
1578
|
+
* un `dialect: "sqlite"` assumé que pour une absence de déclaration. Distinguer
|
|
1579
|
+
* les deux est tout l'enjeu — sans cela, l'infra déclarée ne pourrait jamais
|
|
1580
|
+
* s'appliquer, puisqu'un défaut lui ressemblerait toujours à un choix.
|
|
1581
|
+
*/
|
|
1582
|
+
function declaresDialect(projectRoot, writer, connector) {
|
|
1583
|
+
const configPath = path.join(projectRoot, "nodefony.config.ts");
|
|
1584
|
+
if (!writer.exists(configPath)) return false;
|
|
1585
|
+
const block = extractBlock(writer.read(configPath), "connectors");
|
|
1586
|
+
if (block === null) return false;
|
|
1587
|
+
const entry = /(\w+)\s*:\s*\{([^{}]*)\}/gu;
|
|
1588
|
+
let match;
|
|
1589
|
+
while ((match = entry.exec(block)) !== null) {
|
|
1590
|
+
if (match[1] !== connector) continue;
|
|
1591
|
+
return /dialect\s*:\s*["'](\w+)["']/u.test(match[2] ?? "");
|
|
1592
|
+
}
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Dialecte de l'infra DÉCLARÉE du projet, cascade `.env` comprise.
|
|
1597
|
+
*
|
|
1598
|
+
* `process.env` seul ne suffit pas : une URL posée dans `.env.local` (le cas
|
|
1599
|
+
* nominal en développement — le fichier est gitignoré, c'est là qu'on met sa
|
|
1600
|
+
* base) n'est pas dans l'environnement du terminal qui lance le scaffold. On
|
|
1601
|
+
* emprunte donc l'ORDRE de la cascade au runtime ({@link envFileOrder}) plutôt
|
|
1602
|
+
* que d'en inventer un : un ordre affiché qui différerait de l'ordre appliqué
|
|
1603
|
+
* serait pire que pas d'ordre du tout.
|
|
1604
|
+
*
|
|
1605
|
+
* @returns le dialecte SQL, ou `null` si aucune base n'est déclarée (ou si elle
|
|
1606
|
+
* n'est pas SQL — une base mongo ne dicte aucun dialecte à `create entity`).
|
|
1607
|
+
*/
|
|
1608
|
+
function infraDialect(projectRoot) {
|
|
1609
|
+
const env = {};
|
|
1610
|
+
try {
|
|
1611
|
+
for (const name of envFileOrder()) {
|
|
1612
|
+
const file = path.join(projectRoot, name);
|
|
1613
|
+
if (!existsSync(file)) continue;
|
|
1614
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
1615
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u.exec(line);
|
|
1616
|
+
if (!m?.[1]) continue;
|
|
1617
|
+
if (env[m[1]] === void 0) env[m[1]] = m[2]?.trim().replace(/^["']|["']$/gu, "");
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
} catch {}
|
|
1621
|
+
try {
|
|
1622
|
+
const dialect = resolveInfra({
|
|
1623
|
+
...env,
|
|
1624
|
+
...process.env
|
|
1625
|
+
}).database?.dialect;
|
|
1626
|
+
return dialect && ENTITY_DIALECTS.includes(dialect) ? dialect : null;
|
|
1627
|
+
} catch {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Contenu de l'objet qui suit `<clé>:`, borné par son accolade APPARIÉE.
|
|
1633
|
+
*
|
|
1634
|
+
* Sans appariement, une expression régulière lâche continue de trouver des
|
|
1635
|
+
* entrées bien après la fin du bloc visé — elle ramasserait alors tout le reste
|
|
1636
|
+
* du fichier de configuration.
|
|
1637
|
+
*
|
|
1638
|
+
* @returns le corps du bloc (accolades exclues), ou `null` si la clé est absente.
|
|
1639
|
+
*/
|
|
1640
|
+
function extractBlock(source, key) {
|
|
1641
|
+
const at = source.indexOf(key);
|
|
1642
|
+
if (at < 0) return null;
|
|
1643
|
+
const open = source.indexOf("{", at);
|
|
1644
|
+
if (open < 0) return null;
|
|
1645
|
+
let depth = 0;
|
|
1646
|
+
for (let i = open; i < source.length; i += 1) if (source[i] === "{") depth += 1;
|
|
1647
|
+
else if (source[i] === "}") {
|
|
1648
|
+
depth -= 1;
|
|
1649
|
+
if (depth === 0) return source.slice(open + 1, i);
|
|
1650
|
+
}
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Connecteurs déclarés par l'application, lus SANS démarrer le noyau.
|
|
1655
|
+
*
|
|
1656
|
+
* Lecture TEXTUELLE assumée : `nodefony.config.ts` est du TypeScript, et
|
|
1657
|
+
* l'exécuter pour lire une clé coûterait un boot complet. On isole donc le bloc
|
|
1658
|
+
* `connectors: { … }` puis chaque entrée `<nom>: { … dialect: "…" }`.
|
|
1659
|
+
*
|
|
1660
|
+
* Pourquoi ne pas se contenter de chercher le premier `dialect:` du fichier :
|
|
1661
|
+
* une application qui déclare deux connecteurs de moteurs différents ferait
|
|
1662
|
+
* générer TOUTES ses entités dans le dialecte du premier, sans un mot.
|
|
1663
|
+
*
|
|
1664
|
+
* @param projectRoot - racine du projet (là où vit `nodefony.config.ts`).
|
|
1665
|
+
* @param writer - accès fichiers transactionnel du scaffold.
|
|
1666
|
+
* @returns les connecteurs trouvés, dans l'ordre de déclaration (vide si aucun).
|
|
1667
|
+
*/
|
|
1668
|
+
function readConnectors(projectRoot, writer) {
|
|
1669
|
+
const configPath = path.join(projectRoot, "nodefony.config.ts");
|
|
1670
|
+
if (!writer.exists(configPath)) return [];
|
|
1671
|
+
const source = writer.read(configPath);
|
|
1672
|
+
const asDialect = (value) => ENTITY_DIALECTS.includes(value ?? "") ? value : "sqlite";
|
|
1673
|
+
const connectors = [];
|
|
1674
|
+
const block = extractBlock(source, "connectors");
|
|
1675
|
+
if (block !== null) {
|
|
1676
|
+
const entry = /(\w+)\s*:\s*\{([^{}]*)\}/gu;
|
|
1677
|
+
let match;
|
|
1678
|
+
while ((match = entry.exec(block)) !== null) {
|
|
1679
|
+
const [, name, body] = match;
|
|
1680
|
+
if (!name) continue;
|
|
1681
|
+
const ddl = /ddl\s*:\s*["'](\w+)["']/u.exec(body ?? "")?.[1];
|
|
1682
|
+
connectors.push({
|
|
1683
|
+
name,
|
|
1684
|
+
dialect: asDialect(/dialect\s*:\s*["'](\w+)["']/u.exec(body ?? "")?.[1]),
|
|
1685
|
+
...ddl !== void 0 ? { ddl } : {}
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (connectors.length > 0) return connectors;
|
|
1690
|
+
return [{
|
|
1691
|
+
name: "default",
|
|
1692
|
+
dialect: asDialect(/dialect\s*:\s*["'](\w+)["']/u.exec(source)?.[1])
|
|
1693
|
+
}];
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Entités déjà déclarées dans une cible, lues au disque.
|
|
1697
|
+
*
|
|
1698
|
+
* Sert à transformer `ref:` d'un texte libre en un CHOIX : le formulaire de
|
|
1699
|
+
* Studio comme le dialogue du terminal peuvent proposer ce qui existe vraiment,
|
|
1700
|
+
* au lieu de laisser deviner un nom qui fera échouer le démarrage.
|
|
1701
|
+
*/
|
|
1702
|
+
function readEntities(targetDir, writer) {
|
|
1703
|
+
const dir = path.join(targetDir, "nodefony", "entity");
|
|
1704
|
+
if (!writer.exists(dir)) return [];
|
|
1705
|
+
return writer.listDir(dir).filter((file) => !file.isDirectory && file.name.endsWith(".ts") && !file.name.endsWith(".schema.ts")).map((file) => file.name.slice(0, -3)).sort();
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Scaffold IN-PROJECT d'une entité (`create entity`) : table Drizzle native, interface
|
|
1709
|
+
* de ligne, schémas Zod d'entrée, service CRUD, controller REST + WebSocket, tests.
|
|
1710
|
+
*
|
|
1711
|
+
* Câble la cible : `@entities([...])` (créé au besoin) et `@controllers([...])`.
|
|
1712
|
+
*
|
|
1713
|
+
* @throws hors projet · cible inconnue · aucun module ORM dans les dépendances ·
|
|
1714
|
+
* entité déjà déclarée · champ mal formé.
|
|
1715
|
+
*/
|
|
1716
|
+
function runEntityScaffold(request, answers, packageRoot, writer) {
|
|
1717
|
+
const projectRoot = findProjectRoot(request.dir);
|
|
1718
|
+
if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
|
|
1719
|
+
const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
|
|
1720
|
+
const manifestPath = path.join(target.dir, "package.json");
|
|
1721
|
+
const manifest = JSON.parse(writer.read(manifestPath));
|
|
1722
|
+
const depsOf = (dir) => {
|
|
1723
|
+
const file = path.join(dir, "package.json");
|
|
1724
|
+
if (!writer.exists(file)) return /* @__PURE__ */ new Set();
|
|
1725
|
+
const pkg = JSON.parse(writer.read(file));
|
|
1726
|
+
return new Set([
|
|
1727
|
+
"dependencies",
|
|
1728
|
+
"devDependencies",
|
|
1729
|
+
"peerDependencies"
|
|
1730
|
+
].flatMap((b) => Object.keys(pkg[b] ?? {})));
|
|
1731
|
+
};
|
|
1732
|
+
const targetDeps = depsOf(target.dir);
|
|
1733
|
+
const projectDeps = depsOf(projectRoot);
|
|
1734
|
+
if (!targetDeps.has("@nodefony/drizzle") && !projectDeps.has("@nodefony/drizzle")) throw new Error(`@nodefony/drizzle absent de ${target.name} — ajoute la dep + use("@nodefony/drizzle") au manifeste modules de nodefony.config.ts, puis relance`);
|
|
1735
|
+
const ormRuntimeNote = [];
|
|
1736
|
+
const depsAdded = [];
|
|
1737
|
+
{
|
|
1738
|
+
const rootManifestPath = path.join(projectRoot, "package.json");
|
|
1739
|
+
let rootManifest = null;
|
|
1740
|
+
const ensure = (section, dep, why) => {
|
|
1741
|
+
if (projectDeps.has(dep)) return;
|
|
1742
|
+
rootManifest ??= JSON.parse(writer.read(rootManifestPath));
|
|
1743
|
+
rootManifest[section] ??= {};
|
|
1744
|
+
rootManifest[section][dep] = SCAFFOLD_VERSIONS[dep];
|
|
1745
|
+
depsAdded.push(dep);
|
|
1746
|
+
ormRuntimeNote.push(`dépendance manquante ajoutée au package.json : ${dep}@${SCAFFOLD_VERSIONS[dep]} (${why})`);
|
|
1747
|
+
};
|
|
1748
|
+
ensure("dependencies", "drizzle-orm", "l'entité l'importe en direct");
|
|
1749
|
+
ensure("devDependencies", "drizzle-kit", "`nodefony orm:generate` le pilote pour écrire les migrations");
|
|
1750
|
+
if (rootManifest !== null) writer.write(rootManifestPath, `${JSON.stringify(rootManifest, null, 2)}\n`);
|
|
1751
|
+
}
|
|
1752
|
+
const base = String(answers.name).replace(/[-_]?[Ee]ntity$/u, "");
|
|
1753
|
+
const pascal = toPascalCase(base);
|
|
1754
|
+
const reserved = findReservedEntity(pascal);
|
|
1755
|
+
if (reserved && !reserved.appOwned) throw new Error(`create entity ${pascal} : ce nom appartient au module « ${reserved.module} » (entité « ${reserved.name} ») — deux entités du même nom ne cohabitent pas dans le registre ORM, et l'application ne démarrerait plus.\n → ${reserved.advice}`);
|
|
1756
|
+
const isUserEntity = reserved?.appOwned === true;
|
|
1757
|
+
if (isUserEntity && target.kind !== "app") throw new Error(`create entity ${pascal} : l'entité de l'utilisateur ne peut vivre que dans l'application RACINE, pas dans le module « ${target.name} ». L'ordre de chargement des modules n'est pas garanti : posée ailleurs, elle arriverait parfois APRÈS celle du framework, et le registre d'entités refuserait le doublon par une exception au démarrage.\n → relancer sans « --module »`);
|
|
1758
|
+
if (isUserEntity) {
|
|
1759
|
+
const refuse = (option, value, impose) => {
|
|
1760
|
+
throw new Error(`create entity ${pascal} : « ${option} ${value} » est refusé — ${impose}. Le framework écrit ces noms en dur dans ses requêtes (recherche par compte externe, listing) : une divergence ne lèverait pas, elle rendrait simplement des résultats vides.\n → relancer sans « ${option} »`);
|
|
1761
|
+
};
|
|
1762
|
+
if (String(answers.table ?? "").trim()) refuse("--table", String(answers.table), "la table s'appelle « User »");
|
|
1763
|
+
if (answers.columnCase !== void 0 && answers.columnCase !== "camel") refuse("--column-case", String(answers.columnCase), "les colonnes du contrat sont en camelCase");
|
|
1764
|
+
if (String(answers.id) === "serial") refuse("--id", String(answers.id), "l'identifiant de l'utilisateur est un UUID (un entier auto-incrémenté est devinable, donc énumérable)");
|
|
1765
|
+
if (answers.idName !== void 0 && String(answers.idName) !== "id") refuse("--id-name", String(answers.idName), "la clé primaire s'appelle « id »");
|
|
1766
|
+
}
|
|
1767
|
+
const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
1768
|
+
const kebab = toKebabCase(base);
|
|
1769
|
+
const tableAnswer = String(answers.table ?? "").trim();
|
|
1770
|
+
const table = isUserEntity ? userTableName(target.dir) : tableAnswer ? assertSqlName(tableAnswer, "--table") : tableName(pascal);
|
|
1771
|
+
const columnCase = String(answers.columnCase ?? "camel");
|
|
1772
|
+
if (!COLUMN_CASES.includes(columnCase)) throw new Error(`--column-case invalide « ${columnCase} » — attendus : ${COLUMN_CASES.join(" | ")}`);
|
|
1773
|
+
const idName = assertSqlName(String(answers.idName ?? "id").trim() || "id", "--id-name");
|
|
1774
|
+
const connector = String(answers.connector || "default");
|
|
1775
|
+
const dialect = String(answers.dialect || "") || detectDialect(projectRoot, writer, connector);
|
|
1776
|
+
if (!ENTITY_DIALECTS.includes(dialect)) throw new Error(`dialecte invalide « ${dialect} » — attendus : ${ENTITY_DIALECTS.join(" | ")}`);
|
|
1777
|
+
const id = String(answers.id);
|
|
1778
|
+
if (!ENTITY_ID_KINDS.includes(id)) throw new Error(`--id invalide « ${id} » — attendus : ${ENTITY_ID_KINDS.join(" | ")}`);
|
|
1779
|
+
const requested = parseEntityFields(String(answers.fields ?? ""));
|
|
1780
|
+
const contract = isUserEntity ? readUserContract(target.dir) : null;
|
|
1781
|
+
if (isUserEntity) assertUserFieldsAreFillable(requested);
|
|
1782
|
+
const fields = contract ? [...userContractFields(contract), ...requested] : requested;
|
|
1783
|
+
if (fields.length === 0) throw new Error(`create entity ${pascal} : aucun champ déclaré — passe-les en arguments (ex : nodefony create entity ${pascal} title:string! body:text? views:int=0)`);
|
|
1784
|
+
const timestamps = isUserEntity ? false : answers.timestamps !== false;
|
|
1785
|
+
const softDelete = answers.softDelete === true;
|
|
1786
|
+
const toList = (value) => Array.isArray(value) ? value.map(String) : typeof value === "string" && value.length > 0 ? [value] : [];
|
|
1787
|
+
const indexes = [...parseEntityIndexes(toList(answers.index), fields, {
|
|
1788
|
+
timestamps,
|
|
1789
|
+
softDelete
|
|
1790
|
+
}), ...parseEntityIndexes(toList(answers.uniqueIndex), fields, {
|
|
1791
|
+
timestamps,
|
|
1792
|
+
softDelete
|
|
1793
|
+
}, true)];
|
|
1794
|
+
const codegen = buildEntityCodegen(fields, {
|
|
1795
|
+
dialect,
|
|
1796
|
+
id,
|
|
1797
|
+
timestamps,
|
|
1798
|
+
softDelete,
|
|
1799
|
+
table,
|
|
1800
|
+
indexes,
|
|
1801
|
+
columnCase,
|
|
1802
|
+
idName
|
|
1803
|
+
});
|
|
1804
|
+
const sortable = [
|
|
1805
|
+
"id",
|
|
1806
|
+
...fields.filter((f) => f.type !== "json").map((f) => f.name),
|
|
1807
|
+
...timestamps ? ["createdAt", "updatedAt"] : []
|
|
1808
|
+
];
|
|
1809
|
+
const filterDef = (f) => {
|
|
1810
|
+
if (f.type === "bool") return "\"boolean\"";
|
|
1811
|
+
if (f.type === "enum" && f.values?.length) return JSON.stringify(f.values).replace(/","/g, "\", \"");
|
|
1812
|
+
if (f.type === "ref") return id === "serial" ? "\"int\"" : "\"string\"";
|
|
1813
|
+
return null;
|
|
1814
|
+
};
|
|
1815
|
+
const filters = fields.map((f) => ({
|
|
1816
|
+
name: f.name,
|
|
1817
|
+
def: filterDef(f)
|
|
1818
|
+
})).filter((e) => e.def !== null);
|
|
1819
|
+
const defaultOrder = timestamps ? "[[\"createdAt\", \"DESC\"], [\"id\", \"DESC\"]]" : "[[\"id\", \"DESC\"]]";
|
|
1820
|
+
for (const field of fields) {
|
|
1821
|
+
if (field.type !== "ref" || !field.target) continue;
|
|
1822
|
+
const targetFile = path.join(target.dir, "nodefony", "entity", `${field.target}.ts`);
|
|
1823
|
+
if (!writer.exists(targetFile)) throw new Error(`create entity ${pascal} : la relation « ${field.name}:ref:${field.target} » vise une entité qui n'existe pas dans ${target.name} — crée-la d'abord (nodefony create entity ${field.target} …), puis relance`);
|
|
1824
|
+
}
|
|
1825
|
+
const relations = fields.filter((f) => f.type === "ref" && f.target).map((f) => ({
|
|
1826
|
+
field: f.name,
|
|
1827
|
+
target: f.target,
|
|
1828
|
+
type: "many-to-one",
|
|
1829
|
+
foreignKey: f.name
|
|
1830
|
+
}));
|
|
1831
|
+
const route = String(answers.route) || `/api/${pluralize(kebab)}`;
|
|
1832
|
+
const sample = {};
|
|
1833
|
+
const factory = [];
|
|
1834
|
+
for (const f of fields) {
|
|
1835
|
+
if (f.nullable) continue;
|
|
1836
|
+
const { fixed, expr } = sampleValue(f, id);
|
|
1837
|
+
sample[f.name] = fixed;
|
|
1838
|
+
factory.push(`${f.name}: ${expr}`);
|
|
1839
|
+
}
|
|
1840
|
+
const comparableField = fields.find((f) => !f.nullable && f.type !== "date" && f.type !== "json" && f.type !== "ref" && f.type !== "enum")?.name ?? null;
|
|
1841
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
1842
|
+
const written = [];
|
|
1843
|
+
const data = {
|
|
1844
|
+
pascal,
|
|
1845
|
+
camel,
|
|
1846
|
+
kebab,
|
|
1847
|
+
table,
|
|
1848
|
+
route,
|
|
1849
|
+
connector,
|
|
1850
|
+
dialect,
|
|
1851
|
+
moduleName: target.kind === "app" ? "app" : String(target.name),
|
|
1852
|
+
curlBody: JSON.stringify(sample),
|
|
1853
|
+
sampleFactory: `{ ${factory.join(", ")} }`,
|
|
1854
|
+
comparableField,
|
|
1855
|
+
hasUnique: fields.some((f) => f.unique),
|
|
1856
|
+
sortProbe: comparableField ?? "id",
|
|
1857
|
+
filterProbe: filterProbe(fields),
|
|
1858
|
+
malformedProbe: malformedProbe(filters),
|
|
1859
|
+
relationTargets: [...new Set(fields.filter((f) => f.type === "ref" && f.target).map((f) => f.target))],
|
|
1860
|
+
timestamps,
|
|
1861
|
+
softDelete,
|
|
1862
|
+
sortable,
|
|
1863
|
+
filters,
|
|
1864
|
+
defaultOrder,
|
|
1865
|
+
relations,
|
|
1866
|
+
hasSecurity: targetDeps.has("@nodefony/security"),
|
|
1867
|
+
...codegen
|
|
1868
|
+
};
|
|
1869
|
+
const templates = path.join(packageRoot, "templates", "entity");
|
|
1870
|
+
const tokens = {
|
|
1871
|
+
__PASCAL__: pascal,
|
|
1872
|
+
__KEBAB__: kebab
|
|
1873
|
+
};
|
|
1874
|
+
const service = isUserEntity ? false : answers.service !== false;
|
|
1875
|
+
const controller = service && answers.controller !== false;
|
|
1876
|
+
renderLayer(eta, path.join(templates, "base"), target.dir, data, written, writer, tokens);
|
|
1877
|
+
if (service) renderLayer(eta, path.join(templates, "service"), target.dir, data, written, writer, tokens);
|
|
1878
|
+
if (controller) renderLayer(eta, path.join(templates, "controller"), target.dir, data, written, writer, tokens);
|
|
1879
|
+
if (!isUserEntity && answers.tests !== false) renderLayer(eta, path.join(templates, "tests"), target.dir, data, written, writer, tokens);
|
|
1880
|
+
if (target.kind === "module") {
|
|
1881
|
+
const peer = manifest["peerDependencies"] ??= {};
|
|
1882
|
+
let touched = false;
|
|
1883
|
+
for (const brick of ["@nodefony/orm-core", "@nodefony/drizzle"]) if (!targetDeps.has(brick)) {
|
|
1884
|
+
peer[brick] = "*";
|
|
1885
|
+
touched = true;
|
|
1886
|
+
}
|
|
1887
|
+
if (touched) {
|
|
1888
|
+
writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1889
|
+
written.push("package.json");
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const indexPath = path.join(target.dir, "index.ts");
|
|
1893
|
+
wireEntitiesDecorator(indexPath, `${pascal}Entity`, `./nodefony/entity/${pascal}`, writer, isUserEntity);
|
|
1894
|
+
if (controller) wireDecoratorList(indexPath, "controllers", `${pascal}Controller`, `./nodefony/controllers/${pascal}Controller`, writer);
|
|
1895
|
+
written.push("index.ts");
|
|
1896
|
+
const writtenMode = readConnectors(projectRoot, writer).find((c) => c.name === connector)?.ddl;
|
|
1897
|
+
const schemaBuiltAtBoot = writtenMode !== "none" && writtenMode !== "migrate";
|
|
1898
|
+
const notes = [
|
|
1899
|
+
...ormRuntimeNote,
|
|
1900
|
+
schemaBuiltAtBoot ? `table ${table} sur le connecteur « ${connector} » (${dialect}) — créée au prochain boot en développement` : `table ${table} sur le connecteur « ${connector} » (${dialect}, schéma : ${writtenMode}) — le démarrage ne la crée PAS`,
|
|
1901
|
+
...schemaBuiltAtBoot ? [`⚠ en dev, un champ ajouté qui accepte le vide est posé au prochain boot ; un champ OBLIGATOIRE ne l'est pas — « nodefony orm:reset », ou une migration`, `⚠ production : appliquer les migrations AVANT le déploiement (« nodefony orm:migrate »), jamais depuis le processus qui sert le trafic`] : [
|
|
1902
|
+
`→ écrire la migration : nodefony orm:generate --name <nom>`,
|
|
1903
|
+
`→ l'appliquer : nodefony orm:migrate${connector === "default" ? "" : ` --connector ${connector}`}`,
|
|
1904
|
+
`⚠ tant qu'elle n'est pas appliquée, la colonne n'existe pas en base — les requêtes qui la lisent échoueront`
|
|
1905
|
+
]
|
|
1906
|
+
];
|
|
1907
|
+
if (controller) notes.push(`REST ${route} (GET liste paginée/POST) · ${route}/{id} (GET/PUT/PATCH/DELETE) — les lectures répondent AUSSI par la socket`);
|
|
1908
|
+
if (!service) notes.push("service et controller non générés (--no-service)");
|
|
1909
|
+
else if (!controller) notes.push("controller non généré (--no-controller)");
|
|
1910
|
+
return {
|
|
1911
|
+
dest: target.dir,
|
|
1912
|
+
files: written.sort(),
|
|
1913
|
+
linked: [],
|
|
1914
|
+
notes,
|
|
1915
|
+
depsAdded
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Ajoute une entité au décorateur `@entities([...])` de la cible — **en le créant
|
|
1920
|
+
* s'il n'existe pas encore**.
|
|
1921
|
+
*
|
|
1922
|
+
* Différence avec `wireDecoratorList` : une app n'a aucune raison de porter un
|
|
1923
|
+
* `@entities([])` vide tant qu'elle n'a pas d'entité, et les apps déjà générées n'en
|
|
1924
|
+
* ont pas. Plutôt que d'exiger une ancre (throw) ou de l'imposer à tous les templates,
|
|
1925
|
+
* on **pose le décorateur au premier usage**, juste au-dessus de `@controllers` (ou de
|
|
1926
|
+
* la déclaration de classe s'il n'y en a pas).
|
|
1927
|
+
*
|
|
1928
|
+
* Édition textuelle conservatrice : toute ambiguïté fait échouer avec un geste à
|
|
1929
|
+
* appliquer à la main, plutôt que de corrompre le fichier.
|
|
1930
|
+
*
|
|
1931
|
+
* @param indexPath - `index.ts` de la cible.
|
|
1932
|
+
* @param className - nom du descripteur (`PostEntity`).
|
|
1933
|
+
* @param importPath - chemin relatif du descripteur.
|
|
1934
|
+
* @throws si l'entité est déjà référencée, ou si aucun point d'insertion n'est trouvé.
|
|
1935
|
+
*/
|
|
1936
|
+
/**
|
|
1937
|
+
* Position d'insertion devant la classe `… extends Module` — décorateurs de
|
|
1938
|
+
* classe COMPRIS, puisque `@entities([…])` doit se poser au-dessus d'eux.
|
|
1939
|
+
*
|
|
1940
|
+
* Volontairement PAS une expression régulière. Le motif naturel
|
|
1941
|
+
* (`(?:@\w+\([\s\S]*?\)\s*)*class …`) est ambigu — le contenu paresseux d'un
|
|
1942
|
+
* décorateur peut se découper de plusieurs façons entre deux itérations — donc
|
|
1943
|
+
* exponentiel au backtracking : mesuré ici, 26 décorateurs sans classe à trouver
|
|
1944
|
+
* coûtaient 636 ms, et chaque paire supplémentaire multipliait par ~3,5. Or le
|
|
1945
|
+
* pire cas EST le cas d'échec prévu par l'appelant (ancre introuvable) : la
|
|
1946
|
+
* commande figeait au lieu de rendre son geste manuel.
|
|
1947
|
+
*
|
|
1948
|
+
* Le balayage ci-dessous remonte les décorateurs un par un en appariant les
|
|
1949
|
+
* parenthèses — linéaire, et correct sur un décorateur qui en contient
|
|
1950
|
+
* lui-même (`@entities([defineEntity(x)])`).
|
|
1951
|
+
*
|
|
1952
|
+
* @param source - contenu de l'`index.ts` de la cible.
|
|
1953
|
+
* @returns l'index d'insertion, ou `undefined` si aucune classe `extends Module`.
|
|
1954
|
+
*/
|
|
1955
|
+
function findModuleClassAnchor(source) {
|
|
1956
|
+
const match = /^class\s+\w+\s+extends\s+Module\b/mu.exec(source);
|
|
1957
|
+
if (!match || match.index === void 0) return;
|
|
1958
|
+
let at = match.index;
|
|
1959
|
+
for (;;) {
|
|
1960
|
+
let end = at - 1;
|
|
1961
|
+
while (end >= 0 && /\s/u.test(source[end])) end--;
|
|
1962
|
+
if (end < 0 || source[end] !== ")") return at;
|
|
1963
|
+
let depth = 0;
|
|
1964
|
+
let open = end;
|
|
1965
|
+
for (; open >= 0; open--) {
|
|
1966
|
+
const char = source[open];
|
|
1967
|
+
if (char === ")") depth++;
|
|
1968
|
+
else if (char === "(") {
|
|
1969
|
+
depth--;
|
|
1970
|
+
if (depth === 0) break;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (open < 0) return at;
|
|
1974
|
+
let name = open - 1;
|
|
1975
|
+
while (name >= 0 && /[\w.]/u.test(source[name])) name--;
|
|
1976
|
+
if (name < 0 || source[name] !== "@" || name === open - 1) return at;
|
|
1977
|
+
at = name;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Câble un descripteur d'entité au module de la cible.
|
|
1982
|
+
*
|
|
1983
|
+
* @param indexPath - `index.ts` de la cible (app racine ou module).
|
|
1984
|
+
* @param className - nom exporté du descripteur.
|
|
1985
|
+
* @param importPath - chemin d'import, en `/`.
|
|
1986
|
+
* @param writer - écrivain du scaffold (respecte `--dry-run`).
|
|
1987
|
+
* @param alreadyWiredIsNormal - `true` quand RÉGÉNÉRER l'entité est le geste
|
|
1988
|
+
* attendu, et non un doublon. C'est le cas de l'utilisateur : son fichier
|
|
1989
|
+
* PRESCRIT « relance la commande avec tes champs », et l'entité est déjà
|
|
1990
|
+
* câblée depuis la naissance de l'application. Le refus reste entier pour
|
|
1991
|
+
* toutes les autres : deux entités du même nom dans un registre PLAT font
|
|
1992
|
+
* échouer le démarrage.
|
|
1993
|
+
* @throws Si le nom est déjà référencé hors de ce cas, ou si l'`index.ts` n'a
|
|
1994
|
+
* aucun import où s'accrocher.
|
|
1995
|
+
*/
|
|
1996
|
+
function wireEntitiesDecorator(indexPath, className, importPath, writer, alreadyWiredIsNormal = false) {
|
|
1997
|
+
const source = writer.read(indexPath);
|
|
1998
|
+
if (new RegExp(`\\b${className}\\b`, "u").test(source)) {
|
|
1999
|
+
if (alreadyWiredIsNormal) return;
|
|
2000
|
+
throw new Error(`${className} est déjà référencé dans ${indexPath} — choisis un autre nom d'entité`);
|
|
2001
|
+
}
|
|
2002
|
+
const importLine = `import { ${className} } from "${importPath}";`;
|
|
2003
|
+
const manual = ` ${importLine}\n @entities([${className}]) sur la classe Module`;
|
|
2004
|
+
const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
|
|
2005
|
+
if (!last || last.index === void 0) throw new Error(`aucun import trouvé dans ${indexPath} — ajoute à la main :\n${manual}`);
|
|
2006
|
+
const importAt = last.index + last[0].length;
|
|
2007
|
+
const listRe = decoratorListRe("entities");
|
|
2008
|
+
if (listRe.test(source)) {
|
|
2009
|
+
const withImport = source.slice(0, importAt) + `\n${importLine}` + source.slice(importAt);
|
|
2010
|
+
const current = listRe.exec(withImport)[2].trim();
|
|
2011
|
+
const wired = withImport.replace(listRe, `$1@entities([${current ? `${current.replace(/,\s*$/u, "")}, ` : ""}${className}])`);
|
|
2012
|
+
writer.write(indexPath, wired);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const anchorRe = /^@controllers\(/mu;
|
|
2016
|
+
if ((anchorRe.exec(source)?.index ?? findModuleClassAnchor(source)) === void 0) throw new Error(`ni @controllers ni « class … extends Module » dans ${indexPath} — ajoute à la main :\n${manual}`);
|
|
2017
|
+
const needsDecoratorImport = !/\bentities\b[^\n]*@nodefony\/orm-core/u.test(source);
|
|
2018
|
+
const injected = `\n${importLine}` + (needsDecoratorImport ? `\nimport { entities } from "@nodefony/orm-core";` : "");
|
|
2019
|
+
const withImports = source.slice(0, importAt) + injected + source.slice(importAt);
|
|
2020
|
+
const shiftedAt = anchorRe.exec(withImports)?.index ?? findModuleClassAnchor(withImports);
|
|
2021
|
+
if (shiftedAt === void 0) throw new Error(`point d'insertion perdu dans ${indexPath} — ajoute à la main :\n${manual}`);
|
|
2022
|
+
const wired = withImports.slice(0, shiftedAt) + `@entities([${className}])\n` + withImports.slice(shiftedAt);
|
|
2023
|
+
writer.write(indexPath, wired);
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Scaffold IN-PROJECT d'un frontend Vite (`create front`) : pose la coquille
|
|
2027
|
+
* HTML (la MÊME brique que `create app`), l'entry du framework choisi, le
|
|
2028
|
+
* controller de page (`renderDocument` + nonce CSP) et le registrar d'entry ;
|
|
2029
|
+
* câble `@controllers` et le hook `onKernelBoot` de la cible ; complète les
|
|
2030
|
+
* deps npm du framework (catalogue UNIQUE `FRONTEND_PARAMS`).
|
|
2031
|
+
*
|
|
2032
|
+
* @throws hors projet, cible inconnue, cible portant DÉJÀ un front
|
|
2033
|
+
* (`frontend/index.html` — ce scaffold pose l'INITIAL, il ne fusionne pas),
|
|
2034
|
+
* ou dep `@nodefony/frontend` absente de l'APPLICATION. Absente du seul module
|
|
2035
|
+
* visé, elle y est POSÉE en peer : un workspace résout depuis l'arbre de
|
|
2036
|
+
* l'app, et exiger une édition manuelle du `package.json` revenait à demander
|
|
2037
|
+
* à la main ce que ce scaffold existe pour écrire.
|
|
2038
|
+
*/
|
|
2039
|
+
function runFrontScaffold(request, answers, packageRoot, writer) {
|
|
2040
|
+
const projectRoot = findProjectRoot(request.dir);
|
|
2041
|
+
if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
|
|
2042
|
+
const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
|
|
2043
|
+
if (writer.exists(path.join(target.dir, "frontend", "index.html"))) throw new Error(`${target.name} porte déjà un front (frontend/index.html) — ce scaffold pose la coquille et l'entry INITIALES ; pour une page de plus, ajoute une entry au registerEntry existant et un controller de page`);
|
|
2044
|
+
const manifestPath = path.join(target.dir, "package.json");
|
|
2045
|
+
const manifest = JSON.parse(writer.read(manifestPath));
|
|
2046
|
+
const targetDeps = new Set([
|
|
2047
|
+
"dependencies",
|
|
2048
|
+
"devDependencies",
|
|
2049
|
+
"peerDependencies"
|
|
2050
|
+
].flatMap((b) => Object.keys(manifest[b] ?? {})));
|
|
2051
|
+
let addFrontendPeer = false;
|
|
2052
|
+
if (!targetDeps.has("@nodefony/frontend")) {
|
|
2053
|
+
const appDeps = readAppDependencyNames(projectRoot, writer);
|
|
2054
|
+
if (target.kind === "module" && appDeps.has("@nodefony/frontend")) addFrontendPeer = true;
|
|
2055
|
+
else throw new Error(`@nodefony/frontend manque dans ${target.name} — ajoute la dep + "@nodefony/frontend" au manifeste modules de nodefony.config.ts`);
|
|
2056
|
+
}
|
|
2057
|
+
const frontend = answers.frontend;
|
|
2058
|
+
const front = FRONTEND_PARAMS[frontend];
|
|
2059
|
+
const base = String(answers.name);
|
|
2060
|
+
const pascal = toPascalCase(base);
|
|
2061
|
+
const nameClass = `${pascal}Controller`;
|
|
2062
|
+
const kebab = toKebabCase(base);
|
|
2063
|
+
const route = String(answers.route) || `/${kebab}`;
|
|
2064
|
+
const eta = new Eta(ETA_OPTIONS);
|
|
2065
|
+
const written = [];
|
|
2066
|
+
const data = {
|
|
2067
|
+
nameClass,
|
|
2068
|
+
pascal,
|
|
2069
|
+
kebab,
|
|
2070
|
+
entryName: kebab,
|
|
2071
|
+
route,
|
|
2072
|
+
frontend,
|
|
2073
|
+
front,
|
|
2074
|
+
appName: target.name
|
|
2075
|
+
};
|
|
2076
|
+
const tokens = {
|
|
2077
|
+
__NAME__: nameClass,
|
|
2078
|
+
__PASCAL__: pascal
|
|
2079
|
+
};
|
|
2080
|
+
renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-shell"), target.dir, data, written, writer, tokens);
|
|
2081
|
+
renderLayer(eta, path.join(packageRoot, "templates", "front", "base"), target.dir, data, written, writer, tokens);
|
|
2082
|
+
renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-registrar"), target.dir, data, written, writer, tokens);
|
|
2083
|
+
renderLayer(eta, path.join(packageRoot, "templates", "front", frontend), target.dir, data, written, writer, tokens);
|
|
2084
|
+
renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-entry", frontend), target.dir, data, written, writer, tokens);
|
|
2085
|
+
if (frontend === "angular") renderLayer(eta, path.join(packageRoot, "templates", "shared", "ng-app-tsconfig"), target.dir, data, written, writer, tokens);
|
|
2086
|
+
if (frontend === "vue") renderLayer(eta, path.join(packageRoot, "templates", "shared", "vue-shim"), target.dir, data, written, writer, tokens);
|
|
2087
|
+
if (frontend === "svelte") renderLayer(eta, path.join(packageRoot, "templates", "shared", "svelte-shim"), target.dir, data, written, writer, tokens);
|
|
2088
|
+
const added = [];
|
|
2089
|
+
const addDeps = (block, entries) => {
|
|
2090
|
+
if (Object.keys(entries).length === 0) return;
|
|
2091
|
+
const deps = manifest[block] ??= {};
|
|
2092
|
+
for (const [name, version] of Object.entries(entries)) if (!targetDeps.has(name)) {
|
|
2093
|
+
deps[name] = version;
|
|
2094
|
+
added.push(name);
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
addDeps("dependencies", front.deps);
|
|
2098
|
+
addDeps("devDependencies", front.devDeps);
|
|
2099
|
+
if (addFrontendPeer) addDeps("peerDependencies", { "@nodefony/frontend": "*" });
|
|
2100
|
+
if (added.length > 0) {
|
|
2101
|
+
writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2102
|
+
written.push("package.json");
|
|
2103
|
+
}
|
|
2104
|
+
const indexPath = path.join(target.dir, "index.ts");
|
|
2105
|
+
wireDecoratorList(indexPath, "controllers", nameClass, `./nodefony/controllers/${nameClass}`, writer);
|
|
2106
|
+
const wireNote = wireKernelBootCall(indexPath, `register${pascal}Entry`, `./nodefony/frontend/register${pascal}Entry`, writer);
|
|
2107
|
+
written.push("index.ts");
|
|
2108
|
+
const notes = [
|
|
2109
|
+
`page ${route} (GET — controller ${nameClass}, entry Vite « ${kebab} »)`,
|
|
2110
|
+
...added.length > 0 ? [`deps ajoutées : ${added.join(", ")} → lance npm install`] : [],
|
|
2111
|
+
...wireNote ? [wireNote] : []
|
|
2112
|
+
];
|
|
2113
|
+
return {
|
|
2114
|
+
dest: target.dir,
|
|
2115
|
+
files: written.sort(),
|
|
2116
|
+
linked: [],
|
|
2117
|
+
notes
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Câble un appel dans le hook `onKernelBoot()` de l'`index.ts` cible : import
|
|
2122
|
+
* de la fonction + soit INSERTION d'un hook complet (s'il n'existe pas — le
|
|
2123
|
+
* layout des index générés est connu), soit note actionnable (un hook existant
|
|
2124
|
+
* n'est JAMAIS édité : on ne réécrit pas du code utilisateur).
|
|
2125
|
+
*
|
|
2126
|
+
* @returns note à afficher si un geste manuel reste nécessaire, sinon `null`.
|
|
2127
|
+
*/
|
|
2128
|
+
function wireKernelBootCall(indexPath, fnName, importPath, writer) {
|
|
2129
|
+
const source = writer.read(indexPath);
|
|
2130
|
+
const importLine = `import { ${fnName} } from "${importPath}";`;
|
|
2131
|
+
const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
|
|
2132
|
+
if (!last || last.index === void 0) return `ajoute à la main : ${importLine} + ${fnName}(this); dans onKernelBoot()`;
|
|
2133
|
+
const importAt = last.index + last[0].length;
|
|
2134
|
+
const withImport = source.slice(0, importAt) + `\n${importLine}` + source.slice(importAt);
|
|
2135
|
+
if (/onKernelBoot\s*\(/u.test(withImport)) {
|
|
2136
|
+
writer.write(indexPath, withImport);
|
|
2137
|
+
return `onKernelBoot() existe déjà dans index.ts — ajoute : ${fnName}(this);`;
|
|
2138
|
+
}
|
|
2139
|
+
const hook = `
|
|
2140
|
+
/**
|
|
2141
|
+
* Déclare l'entry frontend auprès du FrontendService — AVANT
|
|
2142
|
+
* onKernelReady pour que le superviseur Vite démarre avec elle.
|
|
2143
|
+
*/
|
|
2144
|
+
override async onKernelBoot(): Promise<this> {
|
|
2145
|
+
${fnName}(this);\n return this;\n }\n`;
|
|
2146
|
+
const closer = /\n\}\s*\n+export default /u.exec(withImport);
|
|
2147
|
+
if (!closer || closer.index === void 0) {
|
|
2148
|
+
writer.write(indexPath, withImport);
|
|
2149
|
+
return `fin de classe introuvable dans index.ts — ajoute à la main le hook :\n override async onKernelBoot(): Promise<this> { ${fnName}(this); return this; }`;
|
|
2150
|
+
}
|
|
2151
|
+
const wired = withImport.slice(0, closer.index) + hook + withImport.slice(closer.index + 1);
|
|
2152
|
+
writer.write(indexPath, wired);
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
//#endregion
|
|
2156
|
+
export { DATABASE_PARAMS, FRONTEND_PARAMS, ensureWorkspaces, filterProbe, findModuleClassAnchor, findPackageRoot, findProjectRoot, getScaffoldContext, linkLocalDeps, listTargets, malformedProbe, resolveAnswers, resolveDatabase, resolveLocalWorkspaces, runScaffold, sampleValue, scaffoldCaps, toKebabCase, toPascalCase, wireCommandCall, wireDecoratorList, wireEntitiesDecorator, wireKernelBootCall, wireModuleManifest, wireRoleHierarchy, writeAgentPointers };
|