For larger infrastructure, it typically makes sense to have several projects. For example, in your case:
• infrastructure -> cluster nodes and basic services (calico and metallb)
• application -> ESPhome
You could also split "infrastructure" into a "(virtual) metal" part and a "foundation" project.
The "application" project uses a stack reference to fetch the kubeconfig that the "infrastructure" project exports (e.g., the "application" dev stack fetches the "infrastructure" dev stack's kubeconfig) and otherwise assumes that this will provide it access to a Kubernetes cluster with calico and metallb up and running.
In the spirit of "things that change together go together," splitting your IaC into projects allows you to evolve and deploy them separately. Usually, the "infrastructure" parts don't change often, so there's no need to run through a state refresh for them every time you want to deploy an update to your application. (When working in a team, this is also the only reliable way to give members control over only "their" parts of the deployment.) Creating and maintaining a Kubernetes cluster and deploying an application are two separate activities, the application should not have to care about the cluster's internals as much as possible.
In general, I find that when I'm starting to add lots of manual dependency declarations and trying to find workarounds for resources to be deployed in the correct sequence, this is a sign that I should split my infra code into separate projects. In small setups, you can still always deploy both projects in sequence, but you ensure that your "infrastructure" is ready by waiting for its "up" to complete, rather than trying to achieve this through dependency declarations within the code.
https://www.pulumi.com/docs/iac/using-pulumi/organizing-projects-stacks/ has more background info and examples.