alloy-di 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +12 -7
  2. package/bin/alloy.js +9 -0
  3. package/dist/cli.d.ts +24 -0
  4. package/dist/cli.js +168 -0
  5. package/dist/generate.d.ts +28 -0
  6. package/dist/generate.js +46 -0
  7. package/dist/lib/container.d.ts +78 -3
  8. package/dist/lib/container.js +130 -6
  9. package/dist/lib/lazy.d.ts +5 -1
  10. package/dist/lib/providers.d.ts +26 -2
  11. package/dist/lib/providers.js +23 -1
  12. package/dist/lib/scope.d.ts +21 -1
  13. package/dist/lib/types.d.ts +3 -1
  14. package/dist/plugins/consumer-plugin.d.ts +40 -0
  15. package/dist/plugins/consumer-plugin.js +101 -0
  16. package/dist/plugins/core/codegen.js +3 -3
  17. package/dist/plugins/core/container-loader.js +32 -17
  18. package/dist/plugins/core/discovery-runtime.js +19 -4
  19. package/dist/plugins/core/discovery-store.js +20 -6
  20. package/dist/plugins/core/generation-inputs.js +41 -0
  21. package/dist/plugins/core/scanner.js +71 -7
  22. package/dist/plugins/core/utils.js +1 -1
  23. package/dist/plugins/core/visualization-utils.js +1 -1
  24. package/dist/plugins/core/visualizer.js +40 -10
  25. package/dist/plugins/rollup-plugin/index.js +3 -5
  26. package/dist/plugins/vite-plugin/index.d.ts +2 -29
  27. package/dist/plugins/vite-plugin/index.js +19 -65
  28. package/dist/plugins/webpack-like-plugin.d.ts +32 -0
  29. package/dist/plugins/webpack-like-plugin.js +86 -0
  30. package/dist/rspack.d.ts +7 -0
  31. package/dist/rspack.js +10 -0
  32. package/dist/runtime.d.ts +6 -6
  33. package/dist/runtime.js +4 -4
  34. package/dist/scopes.d.ts +9 -1
  35. package/dist/scopes.js +29 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/dist/vite.d.ts +2 -1
  38. package/dist/webpack.d.ts +7 -0
  39. package/dist/webpack.js +10 -0
  40. package/package.json +28 -12
  41. package/dist/lib/testing/mocking.d.ts +0 -12
  42. package/dist/lib/testing/mocking.js +0 -107
  43. package/dist/lib/testing/registry.js +0 -17
  44. package/dist/test.d.ts +0 -50
  45. package/dist/test.js +0 -65
@@ -1,6 +1,6 @@
1
1
  import { Newable, Token } from "./types.js";
2
2
  import { ServiceScope } from "./scope.js";
3
- import { Container } from "./container.js";
3
+ import { Container, FactoryFn } from "./container.js";
4
4
  import { Lazy } from "./lazy.js";
5
5
 
6
6
  //#region src/lib/providers.d.ts
@@ -56,11 +56,19 @@ interface LazyServiceProviderDescriptor<T = unknown> {
56
56
  lifecycle: ProviderLifecycle;
57
57
  deps?: DependenciesOption;
58
58
  }
59
+ /** Descriptor for a token-bound factory provider with an explicit lifecycle. */
60
+ interface FactoryProviderDescriptor<T = unknown> {
61
+ kind: "factory";
62
+ token: Token<T>;
63
+ factory: FactoryFn<T>;
64
+ lifecycle: ProviderLifecycle;
65
+ }
59
66
  /** Aggregate provider definitions for batch application. */
60
67
  interface ProviderDefinitions {
61
68
  values?: ValueProviderDescriptor[];
62
69
  services?: ServiceProviderDescriptor[];
63
70
  lazyServices?: LazyPlaceholder[];
71
+ factories?: FactoryProviderDescriptor[];
64
72
  }
65
73
  /**
66
74
  * Identity helper for provider definition blocks.
@@ -76,6 +84,22 @@ declare function asClass<T extends Newable<unknown>>(useClass: T, options: {
76
84
  lifecycle: ProviderLifecycle;
77
85
  deps?: DependenciesOption;
78
86
  }): ServiceProviderDescriptor<T>;
87
+ /**
88
+ * Create a factory provider: bind a token to a function executed at resolution
89
+ * time. Mirrors `asClass` — a token, a factory function, and a lifecycle.
90
+ *
91
+ * The factory receives the resolving context and may be async, so it can
92
+ * resolve its own dependencies. `singleton` caches the result on the root;
93
+ * `transient` re-runs on every resolution; a custom scope caches once per
94
+ * matching scope instance and disposes the result with it.
95
+ *
96
+ * @param token Token the produced value is bound to (its type checks `factory`'s return).
97
+ * @param factory Function invoked with the resolving context; may be async.
98
+ * @param options.lifecycle Lifecycle scope (singleton, transient, or a custom scope).
99
+ */
100
+ declare function asFactory<T>(token: Token<T>, factory: FactoryFn<T>, options: {
101
+ lifecycle: ProviderLifecycle;
102
+ }): FactoryProviderDescriptor<T>;
79
103
  /**
80
104
  * Convenience lifecycle helpers for fluent provider creation.
81
105
  */
@@ -108,4 +132,4 @@ declare function asLazyClass<T, const TDeps extends DependenciesOption>(importer
108
132
  */
109
133
  declare function applyProviders(container: Container, definitions: ProviderDefinitions | ProviderDefinitions[]): void;
110
134
  //#endregion
111
- export { ProviderDefinitions, applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle };
135
+ export { FactoryProviderDescriptor, ProviderDefinitions, applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle };
@@ -34,6 +34,27 @@ function asClass(useClass, options) {
34
34
  };
35
35
  }
36
36
  /**
37
+ * Create a factory provider: bind a token to a function executed at resolution
38
+ * time. Mirrors `asClass` — a token, a factory function, and a lifecycle.
39
+ *
40
+ * The factory receives the resolving context and may be async, so it can
41
+ * resolve its own dependencies. `singleton` caches the result on the root;
42
+ * `transient` re-runs on every resolution; a custom scope caches once per
43
+ * matching scope instance and disposes the result with it.
44
+ *
45
+ * @param token Token the produced value is bound to (its type checks `factory`'s return).
46
+ * @param factory Function invoked with the resolving context; may be async.
47
+ * @param options.lifecycle Lifecycle scope (singleton, transient, or a custom scope).
48
+ */
49
+ function asFactory(token, factory, options) {
50
+ return {
51
+ kind: "factory",
52
+ token,
53
+ factory,
54
+ lifecycle: options.lifecycle
55
+ };
56
+ }
57
+ /**
37
58
  * Convenience lifecycle helpers for fluent provider creation.
38
59
  */
39
60
  const lifecycle = {
@@ -145,6 +166,7 @@ function applyProviders(container, definitions) {
145
166
  detectProviderCycles(planEntries);
146
167
  for (const definition of list) {
147
168
  for (const valueProvider of definition.values ?? []) container.provideValue(valueProvider.token, valueProvider.value);
169
+ for (const factoryProvider of definition.factories ?? []) container.provideFactory(factoryProvider.token, factoryProvider.factory, { lifecycle: factoryProvider.lifecycle });
148
170
  const registerService = (ctor, lifecycle, deps, factory) => {
149
171
  dependenciesRegistry.set(ctor, {
150
172
  scope: lifecycle,
@@ -161,4 +183,4 @@ function applyProviders(container, definitions) {
161
183
  }
162
184
  }
163
185
  //#endregion
164
- export { applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle };
186
+ export { applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle };
@@ -10,16 +10,36 @@ interface AlloyScopes {
10
10
  transient: true;
11
11
  }
12
12
  type ServiceScope = keyof AlloyScopes;
13
+ /**
14
+ * A cached factory result tagged with the `generation` of the descriptor that
15
+ * produced it. A mismatch against the current descriptor's generation (after a
16
+ * factory is re-registered) makes the entry stale, so it is recomputed.
17
+ */
18
+ interface FactoryCacheEntry {
19
+ generation: number;
20
+ value: unknown;
21
+ }
22
+ /** An in-flight factory resolution tagged with its descriptor generation. */
23
+ interface FactoryPendingEntry {
24
+ generation: number;
25
+ promise: Promise<unknown>;
26
+ }
13
27
  interface ResolutionContext {
14
28
  readonly scopeName: ServiceScope;
15
29
  getCached(target: Constructor): unknown;
30
+ hasCached(target: Constructor): boolean;
16
31
  setCached(target: Constructor, instance: unknown): void;
17
32
  getPending(target: Constructor): Promise<unknown> | undefined;
18
33
  setPending(target: Constructor, promise: Promise<unknown>): void;
19
34
  deletePending(target: Constructor): void;
20
35
  getProvider(tokenId: symbol): unknown;
21
36
  hasProvider(tokenId: symbol): boolean;
37
+ getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
38
+ setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
39
+ getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
40
+ setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
41
+ deleteFactoryPending(tokenId: symbol, generation: number): void;
22
42
  readonly parent: ResolutionContext | null;
23
43
  }
24
44
  //#endregion
25
- export { AlloyScopes, ResolutionContext, ServiceScope };
45
+ export { AlloyScopes, FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope };
@@ -18,5 +18,7 @@ interface Token<T> {
18
18
  }
19
19
  /** Create a unique typed token for values or abstractions. */
20
20
  declare function createToken<T>(description?: string): Token<T>;
21
+ declare function isToken(value: unknown): value is Token<unknown>;
22
+ declare function isConstructor(value: unknown): value is Constructor;
21
23
  //#endregion
22
- export { Constructor, Newable, Token, createToken };
24
+ export { Constructor, Newable, Token, createToken, isConstructor, isToken };
@@ -0,0 +1,40 @@
1
+ import { ServiceIdentifier } from "../lib/service-identifiers.js";
2
+ import { AlloyManifest } from "./core/types.js";
3
+ import { AlloyScopesConfig } from "./core/scopes-validation.js";
4
+ import { AlloyMermaidVisualizerOptions, AlloyVisualizationOptions } from "./core/visualization-utils.js";
5
+
6
+ //#region src/plugins/consumer-plugin.d.ts
7
+ interface AlloyPluginOptions {
8
+ providers?: string[];
9
+ /**
10
+ * Source directories to scan for decorated services before the virtual
11
+ * container is loaded. Relative paths are resolved against the project root.
12
+ * Defaults to ["src"].
13
+ */
14
+ sourceDirs?: string[];
15
+ /** Optional list of manifest objects to ingest */
16
+ manifests?: AlloyManifest[];
17
+ /** List of ServiceIdentifiers to mark as instantiation-lazy (adds factory Lazy wrapper) */
18
+ lazyServices?: ServiceIdentifier[];
19
+ /**
20
+ * Output directory for the generated `virtual-container.d.ts` file.
21
+ * Relative paths are resolved against the project root.
22
+ * Defaults to "./src".
23
+ */
24
+ containerDeclarationDir?: string;
25
+ /**
26
+ * Emit dependency graph artifacts. When `true`, writes a Mermaid diagram to
27
+ * `${projectRoot}/alloy-di.mmd`. Provide an object to customize output.
28
+ */
29
+ visualize?: boolean | AlloyVisualizationOptions;
30
+ /**
31
+ * Declares custom, application-defined scopes and their parent ordering, e.g.
32
+ * `{ session: { parent: 'singleton' }, request: { parent: 'session' } }`.
33
+ * Drives type-safe scope names (emitted into the generated declaration),
34
+ * runtime hierarchy registration, and build-time scope-stability validation.
35
+ */
36
+ scopes?: AlloyScopesConfig;
37
+ }
38
+ declare const ALLOY_PLUGIN_OPTIONS: unique symbol;
39
+ //#endregion
40
+ export { ALLOY_PLUGIN_OPTIONS, AlloyPluginOptions };
@@ -0,0 +1,101 @@
1
+ import { normalizeImportPath, writeFileIfChanged } from "./core/utils.js";
2
+ import { resolveVisualizationOptions } from "./core/visualization-utils.js";
3
+ import { loadVirtualContainerModule } from "./core/container-loader.js";
4
+ import { createDiscoveryRuntime, isDiscoverableFile } from "./core/discovery-runtime.js";
5
+ import { DEFAULT_SOURCE_DIRS, readPackageName, scanSourceDirectories, toLazyServiceKey } from "./core/generation-inputs.js";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ //#region src/plugins/consumer-plugin.ts
9
+ const ALLOY_PLUGIN_OPTIONS = Symbol.for("alloy-di.plugin-options");
10
+ function createConsumerPluginContext(options = {}) {
11
+ const configuredProviderEntries = Array.from(options.providers ?? []);
12
+ const configuredSourceDirs = Array.from(options.sourceDirs ?? DEFAULT_SOURCE_DIRS);
13
+ const providerModuleRefs = [];
14
+ const lazyServiceKeys = new Set((options.lazyServices ?? []).map(toLazyServiceKey));
15
+ const discoveryRuntime = createDiscoveryRuntime();
16
+ let resolvedRoot = process.cwd();
17
+ let packageName = "UNKNOWN_PACKAGE";
18
+ let resolvedVisualization = null;
19
+ let isDevMode;
20
+ function configure(config = {}) {
21
+ resolvedRoot = config.root ?? process.cwd();
22
+ isDevMode = config.isDevMode;
23
+ packageName = readPackageName(resolvedRoot);
24
+ providerModuleRefs.length = 0;
25
+ for (const entry of configuredProviderEntries) {
26
+ const absPath = path.isAbsolute(entry) ? entry : path.resolve(resolvedRoot, entry);
27
+ providerModuleRefs.push({
28
+ absPath,
29
+ importPath: normalizeImportPath(absPath)
30
+ });
31
+ }
32
+ resolvedVisualization = resolveVisualizationOptions(options.visualize, resolvedRoot);
33
+ }
34
+ async function loadContainer() {
35
+ return loadVirtualContainerModule({
36
+ localMetas: Array.from(discoveryRuntime.discoveredClasses.values()),
37
+ lazyReferencedClassKeys: discoveryRuntime.lazyReferencedClassKeys,
38
+ manifests: options.manifests ?? [],
39
+ providerImportPaths: providerModuleRefs.map((ref) => ref.importPath),
40
+ factoryProviders: shouldTrackFactoryProviders() ? Array.from(discoveryRuntime.factoryProvidersByFile.values()).flat() : [],
41
+ lazyServiceKeys,
42
+ packageName,
43
+ resolvedRoot,
44
+ containerDeclarationDir: options.containerDeclarationDir,
45
+ resolvedVisualization,
46
+ isDevMode,
47
+ scopes: options.scopes
48
+ });
49
+ }
50
+ function shouldTrackFactoryProviders() {
51
+ return Boolean(resolvedVisualization);
52
+ }
53
+ return {
54
+ options,
55
+ virtualModuleId: "virtual:alloy-container",
56
+ resolvedVirtualModuleId: "\0virtual:alloy-container",
57
+ get root() {
58
+ return resolvedRoot;
59
+ },
60
+ sourceDirs: configuredSourceDirs,
61
+ configure,
62
+ shouldTrackFactoryProviders,
63
+ processTransform(code, id) {
64
+ return discoveryRuntime.processUpdate(id, code, { factoryProviders: shouldTrackFactoryProviders() });
65
+ },
66
+ processFileUpdate(file, code) {
67
+ return discoveryRuntime.processUpdate(file, code, { factoryProviders: shouldTrackFactoryProviders() });
68
+ },
69
+ removeFile(file) {
70
+ return discoveryRuntime.removeDiscoveredFile(file);
71
+ },
72
+ buildStart(addWatchFile) {
73
+ discoveryRuntime.clear();
74
+ for (const ref of providerModuleRefs) {
75
+ addWatchFile?.(ref.absPath);
76
+ if (shouldTrackFactoryProviders()) try {
77
+ const code = fs.readFileSync(ref.absPath, "utf-8");
78
+ discoveryRuntime.processUpdate(ref.absPath, code, { factoryProviders: true });
79
+ } catch {}
80
+ }
81
+ scanSourceDirectories(discoveryRuntime, resolvedRoot, configuredSourceDirs, { factoryProviders: shouldTrackFactoryProviders() });
82
+ },
83
+ loadContainer,
84
+ async writeContainerCache(filePath) {
85
+ const result = await loadContainer();
86
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
87
+ return writeFileIfChanged(filePath, result.code);
88
+ },
89
+ getWatchFiles() {
90
+ return providerModuleRefs.map((ref) => ref.absPath);
91
+ },
92
+ getWatchDirectories() {
93
+ return configuredSourceDirs.map((sourceDir) => path.isAbsolute(sourceDir) ? sourceDir : path.resolve(resolvedRoot, sourceDir));
94
+ }
95
+ };
96
+ }
97
+ function isAlloyDiscoverableFile(file) {
98
+ return isDiscoverableFile(file);
99
+ }
100
+ //#endregion
101
+ export { ALLOY_PLUGIN_OPTIONS, createConsumerPluginContext, isAlloyDiscoverableFile };
@@ -171,7 +171,7 @@ function reconstructOptionsText(meta, importMap, serviceRenames) {
171
171
  const expr = reconstructDependencyExpression(factory, () => "", path.dirname(meta.filePath));
172
172
  parts.push(`factory: ${expr}`);
173
173
  }
174
- if (scope === "singleton") parts.push(`scope: 'singleton'`);
174
+ if (scope && scope !== "transient") parts.push(`scope: '${escapeSingleQuotes(scope)}'`);
175
175
  if (dependencies && dependencies.length > 0) {
176
176
  const depExprs = dependencies.map((dep) => {
177
177
  return reconstructDependencyExpression(dep, (ident) => {
@@ -381,8 +381,8 @@ function generateContainerTypeDefinition(metas, pathResolver) {
381
381
  for (const meta of metas) {
382
382
  const importName = pool.claim(resolver.resolve(meta.className, meta.filePath));
383
383
  const importPath = pathResolver(meta.filePath);
384
- if (importName === meta.className) imports.push(`import { ${meta.className} } from '${importPath}';`);
385
- else imports.push(`import { ${meta.className} as ${importName} } from '${importPath}';`);
384
+ if (importName === meta.className) imports.push(` import { ${meta.className} } from '${importPath}';`);
385
+ else imports.push(` import { ${meta.className} as ${importName} } from '${importPath}';`);
386
386
  const exportKey = createIdentifierExportKey(meta, resolver);
387
387
  interfaceMembers.push(`${exportKey}: ServiceIdentifier<${importName}>;`);
388
388
  }
@@ -5,10 +5,29 @@ import { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateM
5
5
  import { validateScopeStability, validateScopesConfig } from "./scopes-validation.js";
6
6
  import { generateMermaidDiagram } from "./visualizer.js";
7
7
  import { ensureDirectoryForFile } from "./visualization-utils.js";
8
- import path from "node:path";
9
8
  import fs from "node:fs";
9
+ import path from "node:path";
10
10
  //#region src/plugins/core/container-loader.ts
11
11
  async function loadVirtualContainerModule(options) {
12
+ const prepared = await prepareContainerData(options);
13
+ const code = generateContainerModule(prepared.metas, prepared.lazyClassKeys, prepared.providerImports, {
14
+ isDev: options.isDevMode,
15
+ scopes: options.scopes
16
+ });
17
+ writeDeclarationArtifacts({
18
+ metas: prepared.metas,
19
+ loadedManifests: prepared.loadedManifests,
20
+ resolvedRoot: options.resolvedRoot,
21
+ containerDeclarationDir: options.containerDeclarationDir,
22
+ scopes: options.scopes
23
+ });
24
+ writeVisualizationArtifact(prepared.metas, prepared.lazyClassKeys, options.factoryProviders, options.resolvedVisualization, options.scopes);
25
+ return {
26
+ code,
27
+ moduleType: "js"
28
+ };
29
+ }
30
+ async function prepareContainerData(options) {
12
31
  const metas = options.localMetas.map((meta) => ({
13
32
  ...meta,
14
33
  metadata: { ...meta.metadata }
@@ -33,19 +52,13 @@ async function loadVirtualContainerModule(options) {
33
52
  const providerImports = Array.from(new Set([...options.providerImportPaths, ...manifestData.providers]));
34
53
  reconcileLazySet(metas, lazyClassKeys, collectEagerReferencedNames(metas));
35
54
  augmentFactoryLazyServices(metas, options.lazyServiceKeys);
36
- if (options.scopes && Object.keys(options.scopes).length > 0) {
37
- validateScopesConfig(options.scopes);
38
- validateScopeStability(metas, options.scopes);
39
- }
40
- const code = generateContainerModule(metas, lazyClassKeys, providerImports, {
41
- isDev: options.isDevMode,
42
- scopes: options.scopes
43
- });
44
- writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir, options.scopes);
45
- writeVisualizationArtifact(metas, lazyClassKeys, options.resolvedVisualization, options.scopes);
55
+ if (options.scopes && Object.keys(options.scopes).length > 0) validateScopesConfig(options.scopes);
56
+ validateScopeStability(metas, options.scopes);
46
57
  return {
47
- code,
48
- moduleType: "js"
58
+ metas,
59
+ loadedManifests,
60
+ lazyClassKeys,
61
+ providerImports
49
62
  };
50
63
  }
51
64
  function assignIdentifierKeys(metas, packageName, resolvedRoot) {
@@ -70,13 +83,14 @@ function assertNoDuplicateManifestServices(metas, manifestServices) {
70
83
  "Resolve by removing one source (local or manifest) to avoid ambiguous DI keys."
71
84
  ].join("\n"));
72
85
  }
73
- function writeTypeDefinitions(metas, loadedManifests, resolvedRoot, containerDeclarationDir, scopes) {
86
+ function writeDeclarationArtifacts(options) {
87
+ const { metas, loadedManifests, resolvedRoot, containerDeclarationDir } = options;
74
88
  const dtsDir = path.resolve(resolvedRoot, containerDeclarationDir ?? "./src");
75
89
  const dtsContent = generateContainerTypeDefinition(metas, (filePath) => resolveDeclarationImportPath(dtsDir, filePath));
76
90
  if (!fs.existsSync(dtsDir)) fs.mkdirSync(dtsDir, { recursive: true });
77
91
  writeFileIfChanged(path.join(dtsDir, "alloy-container.d.ts"), dtsContent);
78
92
  const scopeAugmentationPath = path.join(dtsDir, "alloy-scopes.d.ts");
79
- const scopeAugmentation = generateScopeAugmentationDefinition(scopes ? Object.keys(scopes) : []);
93
+ const scopeAugmentation = generateScopeAugmentationDefinition(options.scopes ? Object.keys(options.scopes) : []);
80
94
  if (scopeAugmentation) writeFileIfChanged(scopeAugmentationPath, scopeAugmentation);
81
95
  else if (fs.existsSync(scopeAugmentationPath)) fs.rmSync(scopeAugmentationPath);
82
96
  if (loadedManifests.length === 0) return;
@@ -93,11 +107,12 @@ function resolveDeclarationImportPath(dtsDir, filePath) {
93
107
  if (!rel.startsWith(".")) rel = "./" + rel;
94
108
  return rel;
95
109
  }
96
- function writeVisualizationArtifact(metas, lazyReferencedClassKeys, resolvedVisualization, scopes) {
110
+ function writeVisualizationArtifact(metas, lazyReferencedClassKeys, factoryProviders, resolvedVisualization, scopes) {
97
111
  if (!resolvedVisualization) return;
98
112
  const artifact = generateMermaidDiagram({
99
113
  metas,
100
114
  lazyClassKeys: new Set(lazyReferencedClassKeys),
115
+ factoryProviders,
101
116
  options: resolvedVisualization.mermaidOptions,
102
117
  scopes
103
118
  });
@@ -105,4 +120,4 @@ function writeVisualizationArtifact(metas, lazyReferencedClassKeys, resolvedVisu
105
120
  writeFileIfChanged(resolvedVisualization.outputPath, `%% ${GENERATED_FILE_NOTICE}\n${artifact.diagram}\n`);
106
121
  }
107
122
  //#endregion
108
- export { loadVirtualContainerModule };
123
+ export { loadVirtualContainerModule, prepareContainerData, writeDeclarationArtifacts };
@@ -34,31 +34,46 @@ function lazyKeysSignature(keys) {
34
34
  if (!keys || keys.size === 0) return "";
35
35
  return Array.from(keys).toSorted().join("|");
36
36
  }
37
+ function factoryProvidersSignature(providers) {
38
+ return JSON.stringify((providers ?? []).map((provider) => ({
39
+ filePath: provider.filePath,
40
+ tokenExpression: provider.tokenExpression,
41
+ tokenLabel: provider.tokenLabel,
42
+ lifecycle: provider.lifecycle
43
+ })));
44
+ }
37
45
  function createDiscoveryRuntime() {
38
46
  const discovery = createDiscoveryStore();
39
47
  const discoveredClasses = /* @__PURE__ */ new Map();
40
48
  const lazyReferencedClassKeys = /* @__PURE__ */ new Set();
49
+ const factoryProvidersByFile = /* @__PURE__ */ new Map();
41
50
  return {
42
51
  discoveredClasses,
43
52
  lazyReferencedClassKeys,
44
- processUpdate(id, code) {
45
- const { metas, lazyClassKeys, previousMetas, previousLazyClassKeys } = discovery.updateFile(id, code);
53
+ factoryProvidersByFile,
54
+ processUpdate(id, code, options) {
55
+ const trackFactoryProviders = options?.factoryProviders ?? true;
56
+ const { metas, lazyClassKeys, factoryProviders, previousMetas, previousLazyClassKeys, previousFactoryProviders } = discovery.updateFile(id, code, { factoryProviders: trackFactoryProviders });
46
57
  if (previousMetas) for (const meta of previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
47
58
  for (const meta of metas) discoveredClasses.set(createClassKey(meta.filePath, meta.className), meta);
48
59
  if (previousLazyClassKeys) for (const key of previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
49
60
  if (lazyClassKeys.size) for (const key of lazyClassKeys) lazyReferencedClassKeys.add(key);
50
- return metasSignature(previousMetas ?? []) !== metasSignature(metas) || lazyKeysSignature(previousLazyClassKeys) !== lazyKeysSignature(lazyClassKeys);
61
+ if (trackFactoryProviders && factoryProviders.length) factoryProvidersByFile.set(id, factoryProviders);
62
+ else factoryProvidersByFile.delete(id);
63
+ return metasSignature(previousMetas ?? []) !== metasSignature(metas) || lazyKeysSignature(previousLazyClassKeys) !== lazyKeysSignature(lazyClassKeys) || trackFactoryProviders && factoryProvidersSignature(previousFactoryProviders) !== factoryProvidersSignature(factoryProviders);
51
64
  },
52
65
  removeDiscoveredFile(file) {
53
66
  const removed = discovery.removeFile(file);
54
67
  if (removed.previousMetas) for (const meta of removed.previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
55
68
  if (removed.previousLazyClassKeys) for (const key of removed.previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
56
- return Boolean(removed.previousMetas?.length || removed.previousLazyClassKeys?.size);
69
+ factoryProvidersByFile.delete(file);
70
+ return Boolean(removed.previousMetas?.length || removed.previousLazyClassKeys?.size || removed.previousFactoryProviders?.length);
57
71
  },
58
72
  clear() {
59
73
  discovery.clear();
60
74
  discoveredClasses.clear();
61
75
  lazyReferencedClassKeys.clear();
76
+ factoryProvidersByFile.clear();
62
77
  }
63
78
  };
64
79
  }
@@ -19,6 +19,7 @@ function hashContent(code) {
19
19
  function createDiscoveryStore(options = {}) {
20
20
  const fileMetas = /* @__PURE__ */ new Map();
21
21
  const fileLazyRefs = /* @__PURE__ */ new Map();
22
+ const fileFactoryProviders = /* @__PURE__ */ new Map();
22
23
  const fileContentHashes = /* @__PURE__ */ new Map();
23
24
  const fileSources = options.trackSources ? /* @__PURE__ */ new Map() : void 0;
24
25
  /**
@@ -28,28 +29,36 @@ function createDiscoveryStore(options = {}) {
28
29
  * @param id - Module identifier or path.
29
30
  * @param code - Current file contents to analyze.
30
31
  */
31
- function updateFile(id, code) {
32
+ function updateFile(id, code, options) {
33
+ const scanFactoryProviders = options?.factoryProviders ?? true;
32
34
  const previousMetas = fileMetas.get(id);
33
35
  const previousLazyClassKeys = fileLazyRefs.get(id);
34
- const contentHash = hashContent(code);
36
+ const previousFactoryProviders = fileFactoryProviders.get(id);
37
+ const contentHash = hashContent(`${scanFactoryProviders ? "factory:1" : "factory:0"}\0${code}`);
35
38
  if (fileContentHashes.get(id) === contentHash) return {
36
39
  metas: previousMetas ?? [],
37
40
  lazyClassKeys: new Set(previousLazyClassKeys),
41
+ factoryProviders: previousFactoryProviders ?? [],
38
42
  previousMetas,
39
- previousLazyClassKeys
43
+ previousLazyClassKeys,
44
+ previousFactoryProviders
40
45
  };
41
46
  fileContentHashes.set(id, contentHash);
42
47
  if (fileSources) fileSources.set(id, code);
43
- const { metas, lazyClassKeys } = scanSource(code, id);
48
+ const { metas, lazyClassKeys, factoryProviders } = scanSource(code, id, { factoryProviders: scanFactoryProviders });
44
49
  if (metas.length) fileMetas.set(id, metas);
45
50
  else fileMetas.delete(id);
46
51
  if (lazyClassKeys.size) fileLazyRefs.set(id, lazyClassKeys);
47
52
  else fileLazyRefs.delete(id);
53
+ if (factoryProviders.length) fileFactoryProviders.set(id, factoryProviders);
54
+ else fileFactoryProviders.delete(id);
48
55
  return {
49
56
  metas,
50
57
  lazyClassKeys,
58
+ factoryProviders,
51
59
  previousMetas,
52
- previousLazyClassKeys
60
+ previousLazyClassKeys,
61
+ previousFactoryProviders
53
62
  };
54
63
  }
55
64
  /**
@@ -60,13 +69,16 @@ function createDiscoveryStore(options = {}) {
60
69
  function removeFile(id) {
61
70
  const previousMetas = fileMetas.get(id);
62
71
  const previousLazyClassKeys = fileLazyRefs.get(id);
72
+ const previousFactoryProviders = fileFactoryProviders.get(id);
63
73
  fileMetas.delete(id);
64
74
  fileLazyRefs.delete(id);
75
+ fileFactoryProviders.delete(id);
65
76
  fileContentHashes.delete(id);
66
77
  if (fileSources) fileSources.delete(id);
67
78
  return {
68
79
  previousMetas,
69
- previousLazyClassKeys
80
+ previousLazyClassKeys,
81
+ previousFactoryProviders
70
82
  };
71
83
  }
72
84
  /**
@@ -75,12 +87,14 @@ function createDiscoveryStore(options = {}) {
75
87
  function clear() {
76
88
  fileMetas.clear();
77
89
  fileLazyRefs.clear();
90
+ fileFactoryProviders.clear();
78
91
  fileContentHashes.clear();
79
92
  fileSources?.clear();
80
93
  }
81
94
  return {
82
95
  fileMetas,
83
96
  fileLazyRefs,
97
+ fileFactoryProviders,
84
98
  fileSources,
85
99
  updateFile,
86
100
  removeFile,
@@ -0,0 +1,41 @@
1
+ import { walkSync } from "./utils.js";
2
+ import { createDiscoveryRuntime } from "./discovery-runtime.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ //#region src/plugins/core/generation-inputs.ts
6
+ const DEFAULT_SOURCE_DIRS = ["src"];
7
+ function toLazyServiceKey(identifier) {
8
+ const description = identifier.description;
9
+ if (!description?.startsWith("alloy:")) throw new Error("[alloy] lazyServices entries must be serviceIdentifiers exported by Alloy manifests.");
10
+ return description;
11
+ }
12
+ function readPackageName(root) {
13
+ try {
14
+ const pkgPath = path.resolve(root, "package.json");
15
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
16
+ if (typeof pkg.name === "string") return pkg.name;
17
+ } catch {}
18
+ return "UNKNOWN_PACKAGE";
19
+ }
20
+ function isSourceFile(file) {
21
+ return /\.tsx?$/i.test(file) && !file.endsWith(".d.ts");
22
+ }
23
+ function scanSourceDirectories(discoveryRuntime, root, sourceDirs = DEFAULT_SOURCE_DIRS, options) {
24
+ for (const sourceDir of sourceDirs) {
25
+ const files = walkSync(path.isAbsolute(sourceDir) ? sourceDir : path.resolve(root, sourceDir));
26
+ for (const file of files) {
27
+ if (!isSourceFile(file)) continue;
28
+ try {
29
+ const code = fs.readFileSync(file, "utf-8");
30
+ discoveryRuntime.processUpdate(file, code, options);
31
+ } catch {}
32
+ }
33
+ }
34
+ }
35
+ function createDiscoveryRuntimeForSourceDirs(root, sourceDirs = DEFAULT_SOURCE_DIRS, options) {
36
+ const discoveryRuntime = createDiscoveryRuntime();
37
+ scanSourceDirectories(discoveryRuntime, root, sourceDirs, options);
38
+ return discoveryRuntime;
39
+ }
40
+ //#endregion
41
+ export { DEFAULT_SOURCE_DIRS, createDiscoveryRuntimeForSourceDirs, readPackageName, scanSourceDirectories, toLazyServiceKey };