Fix app startup issues and add TASK-009 template system refactoring

This commit is contained in:
Tara West
2026-01-08 13:36:03 +01:00
parent 4a1080d547
commit 199b4f9cb2
9 changed files with 482 additions and 63 deletions

View File

@@ -86,6 +86,9 @@ export function NodeGraphContextProvider({ children }: NodeGraphContextProviderP
if (!nodeGraph) return;
function _update(model: ComponentModel) {
// Guard against undefined model (happens on empty projects)
if (!model) return;
if (isComponentModel_CloudRuntime(model)) {
setActive('backend');
if (SidebarModel.instance.ActiveId === 'components') {

View File

@@ -1,4 +1,5 @@
import { filesystem } from '@noodl/platform';
import { bugtracker } from '@noodl-utils/bugtracker';
// TODO: Can we merge this with ProjectModules ?
@@ -27,21 +28,32 @@ export async function listProjectModules(project: TSFixme /* ProjectModel */): P
}[] = [];
const modulesPath = project._retainedProjectDirectory + '/noodl_modules';
const files = await filesystem.listDirectory(modulesPath);
await Promise.all(
files.map(async (file) => {
if (file.isDirectory) {
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
const manifest = await filesystem.readJson(manifestPath);
try {
const files = await filesystem.listDirectory(modulesPath);
modules.push({
name: file.name,
manifest
});
}
})
);
await Promise.all(
files.map(async (file) => {
if (file.isDirectory) {
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
const manifest = await filesystem.readJson(manifestPath);
modules.push({
name: file.name,
manifest
});
}
})
);
} catch (error) {
// noodl_modules folder doesn't exist (fresh/empty project)
if (error.code === 'ENOENT') {
console.log('noodl_modules folder not found (fresh project), skipping module loading');
return [];
}
// Re-throw other errors
throw error;
}
return modules;
}
@@ -50,40 +62,51 @@ export async function readProjectModules(project: TSFixme /* ProjectModel */): P
bugtracker.debug('ProjectModel.readModules');
const modulesPath = project._retainedProjectDirectory + '/noodl_modules';
const files = await filesystem.listDirectory(modulesPath);
project.modules = [];
project.previews = [];
project.componentAnnotations = {};
await Promise.all(
files.map(async (file) => {
if (file.isDirectory) {
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
const manifest = await filesystem.readJson(manifestPath);
try {
const files = await filesystem.listDirectory(modulesPath);
if (manifest) {
manifest.name = file.name;
project.modules.push(manifest);
await Promise.all(
files.map(async (file) => {
if (file.isDirectory) {
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
const manifest = await filesystem.readJson(manifestPath);
if (manifest.componentAnnotations) {
for (var comp in manifest.componentAnnotations) {
var ca = manifest.componentAnnotations[comp];
if (manifest) {
manifest.name = file.name;
project.modules.push(manifest);
if (!project.componentAnnotations[comp]) project.componentAnnotations[comp] = {};
for (var key in ca) project.componentAnnotations[comp][key] = ca[key];
if (manifest.componentAnnotations) {
for (var comp in manifest.componentAnnotations) {
var ca = manifest.componentAnnotations[comp];
if (!project.componentAnnotations[comp]) project.componentAnnotations[comp] = {};
for (var key in ca) project.componentAnnotations[comp][key] = ca[key];
}
}
if (manifest.previews) {
project.previews = manifest.previews.concat(project.previews);
}
}
if (manifest.previews) {
project.previews = manifest.previews.concat(project.previews);
}
}
}
})
);
})
);
console.log(`Loaded ${project.modules.length} modules`);
console.log(`Loaded ${project.modules.length} modules`);
} catch (error) {
// noodl_modules folder doesn't exist (fresh/empty project)
if (error.code === 'ENOENT') {
console.log('noodl_modules folder not found (fresh project), skipping module loading');
return [];
}
// Re-throw other errors
throw error;
}
return project.modules;
}

View File

@@ -260,36 +260,67 @@ export class LocalProjectsModel extends Model {
});
});
} else {
// Default template path
const defaultTemplatePath = './external/projecttemplates/helloworld.zip';
// Check if template exists, otherwise create an empty project
if (filesystem.exists(defaultTemplatePath)) {
this._unzipAndLaunchProject(defaultTemplatePath, dirEntry, fn, options);
} else {
console.warn('Default project template not found, creating empty project');
// Create minimal project.json for empty project
const minimalProject = {
name: name,
components: [],
settings: {}
};
await filesystem.writeFile(filesystem.join(dirEntry, 'project.json'), JSON.stringify(minimalProject, null, 2));
// Load the newly created empty project
projectFromDirectory(dirEntry, (project) => {
if (!project) {
fn();
return;
// Create a minimal Hello World project programmatically
// This is a temporary solution until TASK-009-template-system-refactoring is implemented
const minimalProject = {
name: name,
components: [
{
name: 'App',
ports: [],
visual: true,
visualStateTransitions: [],
nodes: [
{
id: guid(),
type: 'Group',
x: 0,
y: 0,
parameters: {},
ports: [],
children: [
{
id: guid(),
type: 'Text',
x: 50,
y: 50,
parameters: {
text: 'Hello World!'
},
ports: [],
children: []
}
]
}
]
}
],
settings: {},
metadata: {
title: name,
description: 'A new Noodl project'
}
};
project.name = name;
this._addProject(project);
fn(project);
await filesystem.writeFile(filesystem.join(dirEntry, 'project.json'), JSON.stringify(minimalProject, null, 2));
// Load the newly created project
projectFromDirectory(dirEntry, (project) => {
if (!project) {
fn();
return;
}
project.name = name;
this._addProject(project);
project.toDirectory(project._retainedProjectDirectory, (res) => {
if (res.result === 'success') {
fn(project);
} else {
fn();
}
});
}
});
}
}

View File

@@ -3,6 +3,9 @@ import { NodeGraphNode } from '@noodl-models/nodegraphmodel';
import { RuntimeType } from '@noodl-models/nodelibrary/NodeLibraryData';
export function getComponentModelRuntimeType(node: ComponentModel) {
// Guard against undefined node (happens on empty projects)
if (!node) return RuntimeType.Browser;
const name = node.name;
if (name.startsWith('/#__cloud__/')) {

View File

@@ -154,6 +154,11 @@ export async function getPageRoutes(project: ProjectModel, options: IndexedPages
}
});
// Check if traverser has valid root (empty project case)
if (!traverser.root) {
return { routes: [], pages: [], dynamicHash: {} };
}
// Fetch all the Page nodes.
const pages: TraverseNode[] = traverser.filter((node) => node.node.typename === 'Page');

View File

@@ -0,0 +1,54 @@
import path from 'node:path';
import { filesystem } from '@noodl/platform';
import FileSystem from '../../../filesystem';
import { ITemplateProvider, TemplateItem, TemplateListFilter } from '../template';
/**
* Provides access to locally bundled project templates.
* This provider is used for templates that ship with the editor.
*/
export class LocalTemplateProvider implements ITemplateProvider {
get name(): string {
return 'local-templates';
}
async list(_options: TemplateListFilter): Promise<readonly TemplateItem[]> {
// Return only the Hello World template
return [
{
title: 'Hello World',
category: 'Getting Started',
desc: 'A simple starter project to begin your Noodl journey',
iconURL: './assets/template-hello-world-icon.png',
projectURL: 'local://hello-world',
cloudServicesTemplateURL: undefined
}
];
}
canDownload(url: string): Promise<boolean> {
// Handle local:// protocol
return Promise.resolve(url.startsWith('local://'));
}
async download(url: string, destination: string): Promise<void> {
if (url === 'local://hello-world') {
// The template is in project-examples folder at the repository root
// Use process.cwd() which points to repository root during development
const repoRoot = process.cwd();
const sourcePath = path.join(repoRoot, 'project-examples', 'version 1.1.0', 'template-project');
if (!filesystem.exists(sourcePath)) {
throw new Error('Hello World template not found at: ' + sourcePath);
}
// Copy the template folder to destination
// The destination is expected to be where unzipped content goes
// So we copy the folder contents directly
FileSystem.instance.copyRecursiveSync(sourcePath, destination);
} else {
throw new Error(`Unknown local template: ${url}`);
}
}
}

View File

@@ -59,14 +59,24 @@ export class NodeGraphTraverser {
this.traverseComponent = typeof options.traverseComponent === 'boolean' ? options.traverseComponent : true;
this.tagSelector = typeof options.tagSelector === 'function' ? options.tagSelector : null;
this.root = new TraverseNode(this, null, targetNode || project.getRootNode(), null);
const rootNode = targetNode || project.getRootNode();
// Handle empty projects with no root node
if (!rootNode) {
this.root = null;
return;
}
this.root = new TraverseNode(this, null, rootNode, null);
}
public forEach(callback: (node: TraverseNode) => void) {
if (!this.root) return;
this.root.forEach(callback);
}
public map<T = any>(callback: (node: TraverseNode) => T) {
if (!this.root) return [];
const items: T[] = [];
this.forEach((node) => {
const result = callback(node);
@@ -76,6 +86,7 @@ export class NodeGraphTraverser {
}
public filter(callback: (node: TraverseNode) => boolean) {
if (!this.root) return [];
const items: TraverseNode[] = [];
this.forEach((node) => {
if (callback(node)) items.push(node);