How to add custom asset extension (or a virtual asset)?

How can I add “virtual assets” to content browser?

For example, I have a file in content browser with extension .css, how do I tell the content browser about it?
How to display the icon and the name of asset in Editor WITHOUT creating respective .uasset file (this is important).

I know that Content Browser supports uassets and umaps. And I know that assets are shown when they are deserialized and the engine extracts all the information.

I need a solution without compiling the entire engine.

I wrote something, assets appear, but are not deleted if I delete .css files.

FDelegateHandle FCustomAssetManager::WatcherHandle = FDelegateHandle();

void FCustomAssetManager::Initialize()
{
	WatchDirectory = FPaths::ProjectContentDir();
	
	FDirectoryWatcherModule& DirectoryWatcher = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
	DirectoryWatcher.Get()->RegisterDirectoryChangedCallback_Handle(
		WatchDirectory,
		IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FCustomAssetManager::ScanForCSSFiles),
		FCustomAssetManager::WatcherHandle,
		IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges
	);
}

void FCustomAssetManager::Shutdown()
{
	if (FModuleManager::Get().IsModuleLoaded("DirectoryWatcher"))
	{
		FDirectoryWatcherModule& DirectoryWatcher = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
		DirectoryWatcher.Get()->UnregisterDirectoryChangedCallback_Handle(WatchDirectory, OnDirectoryChangedHandle);
	}
}

void FCustomAssetManager::ScanForCSSFiles(const TArray<FFileChangeData> & Data)
{

	if (Data.Num() == 0)
		return;
	
	for (FFileChangeData File : Data)
	{

		if (FPaths::GetExtension(File.Filename) != "css")
			continue;
		
		FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
		IContentBrowserSingleton& ContentBrowserSingleton = ContentBrowserModule.Get();

		IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
		
		FAssetData VirtualAsset(
			FName(FPaths::GetBaseFilename(File.Filename)),
			FName("/Game"),
			FName(FPaths::GetBaseFilename(File.Filename)),
			FName("CSS")
		);
	
		FAssetRegistryState AssetRegistryState = FAssetRegistryState();
		
		if (File.Action == FFileChangeData::FCA_Removed)
		{
			bool bDummy;
			AssetRegistryState.RemoveAssetData(&VirtualAsset, false, bDummy, bDummy);
			AssetRegistry.AppendState(AssetRegistryState);
		}
		else if (File.Action == FFileChangeData::FCA_Added)
		{
			AssetRegistryState.AddAssetData(&VirtualAsset);
			AssetRegistry.AppendState(AssetRegistryState);
		}
	}
}

your remaining gap (assets appear but are not deleted when the .css files go away) comes from watching the wrong event layer: the content browser item removal is driven by the asset registry, and raw directory watcher delegates fire on disk events, which the asset registry does not consume by itself.

two layers worth separating:

editor-time virtual assets: the supported path is the virtual asset system. you register a virtual asset type, and content browser shows entries for files on disk without uassets. the registry manages creation and removal when files appear and disappear, which is exactly the deletion case you are missing, without you managing lifetime by hand.

manual path (what you are doing): to get deletion right you need to reconcile, not react. keep a map of currently registered virtual items keyed by file path. on each directory watcher event (added, removed, renamed), diff against the map: added paths get registered, removed paths get unregistered explicitly, renames come through as remove plus add. the FDirectoryWatcher delegate gives you the filenames, and a periodic or event-driven rescan of the folder is the safety net for missed events. the pure reaction approach misses deletions whenever the delegate fails to fire (which happens across source control operations and atomic saves, where editors replace files rather than delete them).

practical note: atomic saves and scm operations generate churn the watcher reports in bursts, so debounce reconciliation a short delay after events settle, then rescan the directory and sync the registry to disk state. disk state as the source of truth, the map as a cache, deletions can never get lost.