Scaffold Umbraco V14+ Package Development Project with Vite, TypeScript & Lit

Heya 👋
I wanted to share a quick Powershell script I created to help me scaffold a new project for Umbraco packages developed for V14.

As there are quite a few steps to create the projects and setup the client side tooling with Vite and removing unnecessary files & npm scripts that are not needed.

Just show me the script already

## Setup of an Umbraco V14+ project with Vite, TypeScript & Lit
## Author: Warren Buckley
## HackMakeDo.com

## if project name is not provided, ask for it
if (-not $ProjectName) {
    $ProjectName = Read-Host "Enter a project name"
}

## Variables
$ProjectName = $ProjectName.Replace(" ", ".")
$ProjectNameLower = $ProjectName.ToLower()
$WebsiteProjectName = $ProjectName + '.Website'
$WebsiteProjectCSProj = $WebsiteProjectName + '.csproj'
$RCLProjectName = $ProjectName
$RCLProjectCSProj = $RCLProjectName + '.csproj'
$ClientProjectName = $ProjectName + '.Client'

## Folder Paths
$RootFolder = $PSScriptRoot
$WebsiteProjectPath = Join-Path -Path $PSScriptRoot -ChildPath $WebsiteProjectName
$WebsiteCSProjPath = Join-Path -Path $WebsiteProjectPath -ChildPath $WebsiteProjectCSProj
$RCLProjectPath = Join-Path -Path $PSScriptRoot -ChildPath $RCLProjectName
$RCLCSProjPath = Join-Path -Path $RCLProjectPath -ChildPath $RCLProjectCSProj
$ClientProjectPath = Join-Path -Path $PSScriptRoot -ChildPath $ClientProjectName

## LETS GO !!
## Create a default gitignore file
dotnet new gitignore

## Create a new solution
dotnet new sln -n $ProjectName

## Install Umbraco Templates
dotnet new install Umbraco.Templates

## Create a new project that is a website
dotnet new umbraco -n $WebsiteProjectName

## Create the RCL project for the C# and clienside stuff
dotnet new umbracopackage-rcl -n $ProjectName

## Add the website project to the solution
dotnet sln add $WebsiteCSProjPath

## Add the RCL project to the solution
dotnet sln add $RCLCSProjPath

## Delete the wwwroot/umbraco-package.json file
## The Vite setup will add this file from the client folder stuff
Remove-Item -Path "$RCLProjectPath/wwwroot/umbraco-package.json"

## Add the RCL project to the Website project as a project reference
dotnet add $WebsiteCSProjPath reference $RCLCSProjPath

## Create Vite TypeScript & Lit setup
## For vite to be happy it needs to be lowercase
npx create-vite@latest $ClientProjectName.ToLower() --template lit-ts

## Delete the files from vite setup we dont need
Remove-Item -Path "$ClientProjectPath/public/vite.svg"
Remove-Item -Path "$ClientProjectPath/src/assets" -Recurse
Remove-Item -Path "$ClientProjectPath/src/index.css"
Remove-Item -Path "$ClientProjectPath/src/my-element.ts"
Remove-Item -Path "$ClientProjectPath/index.html"

## create vite.config.ts
$ViteConfig = @"
import { defineConfig } from "vite";

export default defineConfig({
    build: {
        lib: {
            entry: "src/index.ts", // Entrypoint file (registers other manifests)
            formats: ["es"],
            fileName: "$ProjectNameLower",
        },
        outDir: "../$RCLProjectName/wwwroot", // your web component will be saved to the RCL project location and the RCL sets the path as App_Plugins/$ProjectName
        emptyOutDir: true,
        sourcemap: true,
        rollupOptions: {
            external: [/^@umbraco/],
        },
    },
});
"@
$ViteConfigPath = Join-Path -Path $ClientProjectPath -ChildPath "vite.config.ts"
$ViteConfig | Out-File -FilePath $ViteConfigPath -Encoding utf8

## create umbraco-package.json
$UmbracoPackageJson = @"
{
    "`$schema": "../../$WebsiteProjectName/umbraco-package-schema.json",
    "id": "$ProjectNameLower",
    "name": "$ProjectName",
    "allowPackageTelemetry": true,
    "version": "1.0.0",
    "extensions": [
        {
          "name": "$ProjectName EntryPoint",
          "alias": "$ProjectNameLower.entrypoint",
          "type": "backofficeEntryPoint",
          "js": "/app_plugins/$RCLProjectName/$ProjectNameLower.js"
        }
      ]
}
"@
$UmbracoPackageJsonPath = Join-Path -Path $ClientProjectPath -ChildPath "/public/umbraco-package.json"
$UmbracoPackageJson | Out-File -FilePath $UmbracoPackageJsonPath -Encoding utf8

## inside src folder create index.ts
## Its an entrypoint file to register manifests
$IndexTs = @"
import { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {

    console.log('Hello from $ProjectName!');

    // We can register many manifests at once via code 
    // as opposed to a long umbraco-package.json file
    // _extensionRegistry.registerMany([
    //     ...entityActionManifests,
    //     ...modalManifests
    // ]);
};
"@
$IndexTsPath = Join-Path -Path $ClientProjectPath -ChildPath "src/index.ts"
$IndexTs | Out-File -FilePath $IndexTsPath -Encoding utf8

## Add .vscode folder and recommended lit extension
New-Item -Path "$ClientProjectPath/.vscode" -ItemType Directory
$ExtensionsJson = @"
{
    "recommendations": [
        "runem.lit-plugin"
    ]
}
"@
$ExtensionsJsonPath = Join-Path -Path $ClientProjectPath -ChildPath ".vscode/extensions.json"
$ExtensionsJson | Out-File -FilePath $ExtensionsJsonPath -Encoding utf8


## update package.json scripts section
## Remove 'dev' and 'preview' scripts
## Add 'watch' script with 'vite build --watch'
$PackageJsonPath = Join-Path -Path $ClientProjectPath -ChildPath "package.json"
$PackageJson = Get-Content -Path $PackageJsonPath | ConvertFrom-Json
$PackageJson.scripts.PSObject.Properties.Remove("dev")
$PackageJson.scripts.PSObject.Properties.Remove("preview")
$PackageJson.scripts | Add-Member -MemberType NoteProperty -Name "watch" -Value "vite build --watch" -Force
$PackageJson | ConvertTo-Json | Out-File -FilePath $PackageJsonPath -Encoding utf8

## Change directory to JS proj
Set-Location -Path $ClientProjectPath

## Need to run 'npm install --save-dev @umbraco-cms/backoffice'
npm install --save-dev @umbraco-cms/backoffice

## Compile the TS to JS & it will copy it out to the RCL project in the wwwroot folder
npm run build

## Build the VS solution (building will generate the JSON schema for umbraco-package.json that we reference)
Set-Location -Path $RootFolder
dotnet build

A link to a GitHub Gist for the PowerShell script is available here
https://gist.github.com/warrenbuckley/a799a14562a8d8f5be2bf7c6671dc547

What does it do?

Hopefully the comments within the script are kinda self explanatory, however as a quick overview it does the following

  • Creates a gitignore file
  • Creates a new Solution named from the project input given to the script
  • Installs the latest Umbraco.Templates from Nuget so its always upto date
  • Creates a new Umbraco Website project named ProjectName.Website that we will use as the testing site of our package code
  • Creates a new Umbraco RCL project called ProjectName
  • Remove the umbraco=package.json file from the RCL project as we will use Vite and the Javascript client to copy this in
  • Adds the two projects to the Solution
  • Adds a reference to the RCL project to the Website project to help test out our work
  • Uses Vite to scaffold a template for using Lit with TypeScript
  • Removes files we do not need from Vite, such as their SVG logo and other files
  • Creates a vite.config.ts to tell Vite about our entrypoint file and to place the build output into the RCL projects wwwroot folder
  • Creates an umbraco-package.json file that will be copied to the RCL wwwroot folder that uses the entrypoint approach of registering further manifests with code
  • Creates the index.ts entrypoint file which allows us to register more manifests into Umbraco
  • Removes the dev and preview NPM scripts from Vite template
  • Adds a new NPM script to watch the TypeScript files and to recompile the bundle as needed
  • Installs the @umbraco-cms/backoffice NPM package to get completions and typings for our codebase
  • And finally does a build of the dotnet solution
  • Phew…. that was a lot of steps

As you can see its quite a few steps to get up and running hence the script was born.

Creating a custom condition for Umbraco Bellissima aka V14+

In this blog post I will show you how to create a custom condition for your Umbraco manifests. In this example I will show you how we can create a custom condition to show or hide our workspace view (aka previously known as Content/Context apps in previous versions of Umbraco)

The problem

In previous versions of Umbraco up to the current release of V13 we have been able to register dashboards and other UI components for the Backoffice with C# and allowing us to specify custom logic on when these should be loaded and visible to logged in users. With this new evolution of the Backoffice all UI extensions are registered solely in JavaScript, hence we need to use a new extension point, bring in a new extension point called Conditions.

What is a manifest condition?

A manifest condition is code that determines and evaluates custom logic to determine if another manifest extension point of Umbraco V14 should load or not.

Umbraco V14 ships with built in ones such as (This is not an exhaustive list)

  • Umb.Condition.SectionAlias
    Matches against a section alias, allowing you to load other Umbraco extension points only if the current visible/loaded section alias matches
  • Umb.Condition.WorkspaceAlias
    Similar to the SectionAlias condition above, this checks to see if the current workspace you are viewing matches the alias. For example you could be wanting to load extensions inside the Log Viewer found inside the Settings section by matching with the alias of Umb.Workspace.LogViewer
  • Umb.Condition.SectionUserPermission
    This condition, checks if the current logged in user is allowed to have access to a section specified as part of the condition.

To see the full list of conditions available to use, you are able to browse to the Settings Section and navigate to the Extensions tree item to view all loaded extensions. This workspace has a handy filter to select the extension type Condition.

An Example

You can use the Umb.Condition.SectionAlias condition above in a manifest for a dashboard to help decide what sections the dashboard is visible in.

{
  "name": "My.WelcomePackage",
  "version": "0.1.0",
  "extensions": [
    {
      "type": "dashboard",
      "alias": "my.welcome.dashboard",
      "name": "My Welcome Dashboard",
      "js": "/App_Plugins/welcome-dashboard/dist/welcome-dashboard.js",
      "elementName": "my-welcome-dashboard",
      "weight": -1,
      "meta": {
        "label": "Welcome Dashboard",
        "pathname": "welcome-dashboard"
      },
      "conditions": [
        {
          "alias": "Umb.Condition.SectionAlias",
          "match": "Umb.Section.Content"
        }
      ]
    }
  ]
}

What are we going to build?

Below is a condition that uses the Document Workspace Context, when you are viewing/editing a content node and is observing the value of the templateId property.

We use a custom configuration for our condition to have a property called hasTemplateSet which is a Boolean to allow the condition to be configured and the logic inversed to check if the current document does or does not have a template set.

For a condition to pass, we need to set the property permitted to true, that is coming from UmbConditionBase and UmbExtensionCondition

Show me the code

import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbConditionConfigBase, UmbConditionControllerArguments, UmbExtensionCondition } from "@umbraco-cms/backoffice/extension-api";
import { UMB_DOCUMENT_WORKSPACE_CONTEXT  } from '@umbraco-cms/backoffice/document';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';

export class TemplateSetCondition extends UmbConditionBase<TemplateSetConditionConfig> implements UmbExtensionCondition
{
    config: TemplateSetConditionConfig;

    constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<TemplateSetConditionConfig>) {
        super(host, args);
        
        this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT , (workspaceCtx) => {

            // Observe the template ID value in the workspace context
            // So that when it changes value we can re-evaluate the condition
            this.observe(workspaceCtx.templateId, (templateId) => {

                // No template set === null
                // Tempate set we get a GUID back

                // Config says it passes if template IS set (aka NOT null)
                if(this.config.hasTemplateSet && templateId !== null) {
                    this.permitted = true;
                    return;
                }

                // Config says it passes if template is NOT set (aka IS null)
                if(this.config.hasTemplateSet === false && templateId === null) {
                    this.permitted = true;
                    return;
                }

                this.permitted = false;
            });
		});
    }
}

export type TemplateSetConditionConfig = UmbConditionConfigBase<'MyThing.Condition.TemplateSet'> & {
    /**
     * HasTemplateSet
	 * Decide if the current document should have a template set or not
	 *
	 * @example
	 * true
	 */
    hasTemplateSet: boolean;
};

Consuming our Condition

Now we have created our condition and registered it with Umbraco, either with an umbraco-package.json or using an entrypoint file and registering the extension with extensionRegistry.registerMany()

We need a way to tell our WorkspaceView aka the Content/Context app in our content node to be only visible if it has a template set.

import { ManifestWorkspaceView } from "@umbraco-cms/backoffice/extension-registry";
import { TemplateSetConditionConfig } from "../Conditions/mything.condition.templateset.js";

const workspaceView: ManifestWorkspaceView = {
    alias: 'mything.workspaceview',
    name: 'My Thing Workspace View',
    type: 'workspaceView',
    js: () => import('./mything.workspaceview.element.js'),
    weight: 190,
    meta: {
        icon: 'icon-people',
        label: 'My Thing',
        pathname: 'my-thing'
    },
    conditions: [
        {
            alias: 'Umb.Condition.WorkspaceAlias',
            match: 'Umb.Workspace.Document',
        },
        {
            alias: 'MyThing.Condition.TemplateSet',
            hasTemplateSet: true
        } as TemplateSetConditionConfig
    ],
}
export const manifests = [workspaceView];

Summary

I hope this has inspired you to create your own custom conditions for the upcoming Umbraco V14 and help understand how you will add custom logic to show/hide your new Umbraco extension points without the concept of C# to register UI with business logic to control when they show.