forked from pulumi/pulumi
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Experimental/Components] Add infrastructure for component discovery …
…and schema inference (pulumi#18580) Adds the `componentProviderHost` function to host a provider that automatically discovers ComponentResource subclasses and generates a schema. This PR sets up the base infrastructure that searches for the components, but does not do any schema inference besides the component names. Part of pulumi#18367 Ref pulumi#15939
- Loading branch information
Showing
11 changed files
with
402 additions
and
0 deletions.
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
...erimental-components-add-infrastructure-for-component-discovery-and-schema-inference.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
changes: | ||
- type: feat | ||
scope: sdk/nodejs | ||
description: "[Experimental/Components] Add infrastructure for component discovery and schema inference" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// Copyright 2025-2025, Pulumi Corporation. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// The typescript import is used for type-checking only. Do not reference it in the emitted code. | ||
// Use `ts` instead to access typescript library functions. | ||
import typescript from "typescript"; | ||
import * as path from "path"; | ||
|
||
// Use the TypeScript shim which allows us to fallback to a vendored version of | ||
// TypeScript if the user has not installed it. | ||
// TODO: we should consider requiring the user to install TypeScript and not | ||
// rely on the shim. In any case, we should add tests for providers with | ||
// different versions of TypeScript in their dependencies, to ensure the | ||
// analyzer code is compatible with all of them. | ||
const ts: typeof typescript = require("../../typescript-shim"); | ||
|
||
export type ComponentDefinition = { | ||
name: string; | ||
}; | ||
|
||
export type TypeDefinition = { | ||
name: string; | ||
}; | ||
|
||
export type AnalyzeResult = { | ||
components: Record<string, ComponentDefinition>; | ||
typeDefinitons: Record<string, TypeDefinition>; | ||
}; | ||
|
||
export class Analyzer { | ||
private checker: typescript.TypeChecker; | ||
private program: typescript.Program; | ||
private components: Record<string, ComponentDefinition> = {}; | ||
private typeDefinitons: Record<string, TypeDefinition> = {}; | ||
|
||
constructor(dir: string) { | ||
const configPath = `${dir}/tsconfig.json`; | ||
const config = ts.readConfigFile(configPath, ts.sys.readFile); | ||
const parsedConfig = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath)); | ||
this.program = ts.createProgram({ | ||
rootNames: parsedConfig.fileNames, | ||
options: parsedConfig.options, | ||
}); | ||
this.checker = this.program.getTypeChecker(); | ||
} | ||
|
||
public analyze(): AnalyzeResult { | ||
const sourceFiles = this.program.getSourceFiles(); | ||
for (const sourceFile of sourceFiles) { | ||
if (sourceFile.fileName.includes("node_modules") || sourceFile.fileName.endsWith(".d.ts")) { | ||
continue; | ||
} | ||
this.analyseFile(sourceFile); | ||
} | ||
return { | ||
components: this.components, | ||
typeDefinitons: this.typeDefinitons, | ||
}; | ||
} | ||
|
||
private analyseFile(sourceFile: typescript.SourceFile) { | ||
// We intentionally visit only the top-level nodes, because we only | ||
// support components defined at the top-level. We have no way to | ||
// instantiate components defined inside functions or methods. | ||
sourceFile.forEachChild((node) => { | ||
if (ts.isClassDeclaration(node) && this.isPulumiComponent(node) && node.name) { | ||
const componentName = node.name.text; | ||
this.components[componentName] = { | ||
name: componentName, | ||
}; | ||
} | ||
}); | ||
} | ||
|
||
private isPulumiComponent(node: typescript.ClassDeclaration): boolean { | ||
if (!node.heritageClauses) { | ||
return false; | ||
} | ||
|
||
return node.heritageClauses.some((clause) => { | ||
return clause.types.some((clauseNode) => { | ||
const type = this.checker.getTypeAtLocation(clauseNode); | ||
const symbol = type.getSymbol(); | ||
const matchesName = symbol?.escapedName === "ComponentResource"; | ||
const sourceFile = symbol?.declarations?.[0].getSourceFile(); | ||
const matchesSourceFile = | ||
sourceFile?.fileName.endsWith("resource.ts") || sourceFile?.fileName.endsWith("resource.d.ts"); | ||
return matchesName && matchesSourceFile; | ||
}); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright 2025-2025, Pulumi Corporation. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
export * from "./provider"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Copyright 2025-2025, Pulumi Corporation. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import { readFileSync } from "fs"; | ||
import * as path from "path"; | ||
import { ComponentResourceOptions } from "../../resource"; | ||
import { ConstructResult, Provider } from "../provider"; | ||
import { Inputs } from "../../output"; | ||
import { main } from "../server"; | ||
import { generateSchema } from "./schema"; | ||
import { Analyzer } from "./analyzer"; | ||
|
||
class ComponentProvider implements Provider { | ||
packageJSON: Record<string, any>; | ||
version: string; | ||
path: string; | ||
|
||
constructor(readonly dir: string) { | ||
const absDir = path.resolve(dir); | ||
const packStr = readFileSync(`${absDir}/package.json`, { encoding: "utf-8" }); | ||
this.packageJSON = JSON.parse(packStr); | ||
this.version = this.packageJSON.version; | ||
this.path = absDir; | ||
} | ||
|
||
async getSchema(): Promise<string> { | ||
const analyzer = new Analyzer(this.path); | ||
const { components, typeDefinitons } = analyzer.analyze(); | ||
const schema = generateSchema(this.packageJSON, components, typeDefinitons); | ||
return JSON.stringify(schema); | ||
} | ||
|
||
async construct( | ||
name: string, | ||
type: string, | ||
inputs: Inputs, | ||
options: ComponentResourceOptions, | ||
): Promise<ConstructResult> { | ||
throw new Error("Not implemented"); | ||
} | ||
} | ||
|
||
export function componentProviderHost(dirname?: string): Promise<void> { | ||
const args = process.argv.slice(2); | ||
// If dirname is not provided, get it from the call stack | ||
if (!dirname) { | ||
// Get the stack trace | ||
const stack = new Error().stack; | ||
// Parse the stack to get the caller's file | ||
// Stack format is like: | ||
// Error | ||
// at componentProviderHost (.../src/index.ts:3:16) | ||
// at Object.<anonymous> (.../caller/index.ts:4:1) | ||
const callerLine = stack?.split("\n")[2]; | ||
const match = callerLine?.match(/\((.+):[0-9]+:[0-9]+\)/); | ||
if (match?.[1]) { | ||
dirname = path.dirname(match[1]); | ||
} else { | ||
throw new Error("Could not determine caller directory"); | ||
} | ||
} | ||
|
||
const prov = new ComponentProvider(dirname); | ||
return main(prov, args); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// Copyright 2025-2025, Pulumi Corporation. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import { ComponentDefinition, TypeDefinition } from "./analyzer"; | ||
|
||
export type PropertyType = "string" | "integer" | "number" | "boolean" | "array" | "object"; | ||
|
||
/** | ||
* https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/schema/#property | ||
*/ | ||
export interface Property { | ||
type: PropertyType; | ||
items?: Property; | ||
additionalProperties?: Property; | ||
ref?: string; | ||
plain?: boolean; | ||
description?: string; | ||
} | ||
|
||
/** | ||
* https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/schema/#objecttype | ||
*/ | ||
export interface ObjectType { | ||
type: PropertyType; | ||
description?: string; | ||
properties?: { [key: string]: Property }; | ||
required?: string[]; | ||
} | ||
|
||
/** | ||
* https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/schema/#complextype | ||
*/ | ||
export interface ComplexType extends ObjectType { | ||
enum?: string[]; | ||
} | ||
|
||
/** | ||
* https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/schema/#resource | ||
*/ | ||
export interface Resource extends ObjectType { | ||
isComponent?: boolean; | ||
inputProperties?: { [key: string]: Property }; | ||
requiredInputs?: string[]; | ||
} | ||
|
||
/** | ||
* https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/schema/#package | ||
*/ | ||
export interface PackageSpec { | ||
name: string; | ||
version?: string; | ||
description?: string; | ||
resources: { [key: string]: Resource }; | ||
types: { [key: string]: ComplexType }; | ||
language?: { [key: string]: any }; | ||
} | ||
|
||
export function generateSchema( | ||
packageJSON: Record<string, any>, | ||
components: Record<string, ComponentDefinition>, | ||
typeDefinitons: Record<string, TypeDefinition>, | ||
): PackageSpec { | ||
const providerName = packageJSON.name; | ||
const result: PackageSpec = { | ||
name: providerName, | ||
version: packageJSON.version, | ||
description: packageJSON.description, | ||
resources: {}, | ||
types: {}, | ||
language: { | ||
nodejs: { | ||
dependencies: {}, | ||
devDependencies: { | ||
typescript: "^5.0.0", | ||
}, | ||
respectSchemaVersion: true, | ||
}, | ||
python: { | ||
respectSchemaVersion: true, | ||
}, | ||
csharp: { | ||
respectSchemaVersion: true, | ||
}, | ||
java: { | ||
respectSchemaVersion: true, | ||
}, | ||
go: { | ||
respectSchemaVersion: true, | ||
}, | ||
}, | ||
}; | ||
|
||
for (const [name, component] of Object.entries(components)) { | ||
result.resources[`${providerName}:index:${name}`] = { | ||
type: "object", | ||
isComponent: true, | ||
}; | ||
} | ||
|
||
for (const [name, type] of Object.entries(typeDefinitons)) { | ||
result.types[`${providerName}:index:${name}`] = { | ||
type: "object", | ||
}; | ||
} | ||
|
||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
.../integration/component_provider/nodejs/component-provider-host/provider/PulumiPlugin.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
runtime: nodejs |
11 changes: 11 additions & 0 deletions
11
tests/integration/component_provider/nodejs/component-provider-host/provider/component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// Copyright 2025-2025, Pulumi Corporation. All rights reserved. | ||
|
||
import * as pulumi from "@pulumi/pulumi"; | ||
|
||
export interface MyComponentArgs { } | ||
|
||
export class MyComponent extends pulumi.ComponentResource { | ||
constructor(name: string, args: MyComponentArgs, opts?: pulumi.ComponentResourceOptions) { | ||
super("nodejs-component-provider:index:MyComponent", name, args, opts); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
tests/integration/component_provider/nodejs/component-provider-host/provider/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Copyright 2025-2025, Pulumi Corporation. All rights reserved. | ||
|
||
import { componentProviderHost } from "@pulumi/pulumi/provider/experimental"; | ||
|
||
componentProviderHost(); |
6 changes: 6 additions & 0 deletions
6
tests/integration/component_provider/nodejs/component-provider-host/provider/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "nodejs-component-provider", | ||
"description": "Node.js Sample Components", | ||
"version": "1.2.3", | ||
"dependencies": {} | ||
} |
Oops, something went wrong.