Skip to content

Commit bab975d

Browse files
authored
feat(tasks): Support for having dependency tasks (#71)
* Better error handling when bundling scripts fails * Refactor runTask signature * Add support for reading assets in tasks and refactor TaskBundleScripts with the new system * Remove dependencyAssets property * Use new writeAssets property for TaskGenerateServices * Support for running dependency tasks when requesting generated file assets * Lint
1 parent 15a8d4c commit bab975d

23 files changed

+717
-207
lines changed

editor/dist/css.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ body, html{
148148

149149
.project-selector-logo {
150150
background: url(img/logo.svg) no-repeat center;
151-
width: 100px;
152-
height: 100px;
151+
width: 169px;
152+
height: 206px;
153153
}
154154

155155
.project-selector-title {

editor/src/assets/AssetManager.js

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,18 @@ import {InternallyCreatedAsset} from "./InternallyCreatedAsset.js";
6969
* TLiveAsset :
7070
* never :
7171
* never :
72-
* never} AssertionOptionsToLiveAsset
72+
* never} AssetAssertionOptionsToLiveAsset
73+
*/
74+
75+
/**
76+
* @template {AssetAssertionOptions} T
77+
* @typedef {AssetAssertionOptionsToProjectAsset<T> extends infer ProjectAsset ?
78+
* ProjectAsset extends import("./ProjectAsset.js").ProjectAsset<infer ProjectAssetType> ?
79+
* ProjectAssetType extends import("./projectAssetType/ProjectAssetType.js").ProjectAssetType<any, any, infer TFileData, any> ?
80+
* TFileData :
81+
* never :
82+
* never :
83+
* never} AssetAssertionOptionsToReadAssetDataReturn
7384
*/
7485

7586
/** @typedef {(granted: boolean) => void} OnPermissionPromptResultCallback */
@@ -295,6 +306,10 @@ export class AssetManager {
295306
}
296307

297308
/**
309+
* Creates a new asset at the specified directory.
310+
* The name of the file is determined by the asset type and if an asset
311+
* already exists a different name will be chosen for the new asset.
312+
* If you wish to create an asset with a specific name, use {@linkcode registerAsset}.
298313
* @param {string[]} parentPath
299314
* @param {string} assetType
300315
*/
@@ -417,7 +432,9 @@ export class AssetManager {
417432
* @param {import("../util/fileSystems/EditorFileSystem.js").FileSystemExternalChangeEvent} e
418433
*/
419434
async externalChange(e) {
420-
const projectAsset = await this.getProjectAssetFromPath(e.path, this.assetSettingsLoaded);
435+
const projectAsset = await this.getProjectAssetFromPath(e.path, {
436+
registerIfNecessary: this.assetSettingsLoaded,
437+
});
421438
if (projectAsset) {
422439
const guessedType = await ProjectAsset.guessAssetTypeFromFile(this.builtInAssetManager, this.projectAssetTypeManager, this.fileSystem, e.path);
423440
if (guessedType != projectAsset.assetType) {
@@ -548,7 +565,6 @@ export class AssetManager {
548565
if (!projectAsset) return null;
549566
if (assertAssetType) {
550567
const projectAssetTypeConstructor = await projectAsset.getProjectAssetTypeConstructor();
551-
552568
AssetManager.assertProjectAssetIsType(projectAssetTypeConstructor, assertAssetType);
553569
}
554570
return /** @type {AssetAssertionOptionsToProjectAsset<T>} */ (projectAsset);
@@ -609,21 +625,33 @@ export class AssetManager {
609625
}
610626

611627
/**
628+
* @template {AssetAssertionOptions} [T = {}]
612629
* @param {string[]} path
613-
* @param {boolean} registerIfNecessary
630+
* @param {Object} options
631+
* @param {boolean} [options.registerIfNecessary]
632+
* @param {T?} [options.assertionOptions]
614633
*/
615-
async getProjectAssetFromPath(path, registerIfNecessary = true) {
634+
async getProjectAssetFromPath(path, {
635+
registerIfNecessary = true,
636+
assertionOptions = null,
637+
} = {}) {
616638
await this.loadAssetSettings(true);
639+
let projectAsset = null;
617640
for (const asset of this.projectAssets.values()) {
618641
if (AssetManager.testPathMatch(path, asset.path)) {
619-
return asset;
642+
projectAsset = asset;
643+
break;
620644
}
621645
}
622646

623-
if (registerIfNecessary && await this.fileSystem.isFile(path)) {
624-
return await this.registerAsset(path);
647+
if (!projectAsset && registerIfNecessary && await this.fileSystem.isFile(path)) {
648+
projectAsset = await this.registerAsset(path);
625649
}
626-
return null;
650+
if (projectAsset && assertionOptions?.assertAssetType) {
651+
const projectAssetTypeConstructor = await projectAsset.getProjectAssetTypeConstructor();
652+
AssetManager.assertProjectAssetIsType(projectAssetTypeConstructor, assertionOptions.assertAssetType);
653+
}
654+
return /** @type {AssetAssertionOptionsToProjectAsset<T>?} */ (projectAsset);
627655
}
628656

629657
/**
@@ -656,14 +684,14 @@ export class AssetManager {
656684
* @template {AssetAssertionOptions} [T = {}]
657685
* @param {import("../../../src/mod.js").UuidString?} uuid
658686
* @param {T} [assertionOptions]
659-
* @returns {Promise<AssertionOptionsToLiveAsset<T>?>}
687+
* @returns {Promise<AssetAssertionOptionsToLiveAsset<T>?>}
660688
*/
661689
async getLiveAsset(uuid, assertionOptions) {
662690
const projectAsset = await this.getProjectAssetFromUuid(uuid, assertionOptions);
663691
if (!projectAsset) return null;
664692

665693
const liveAsset = await projectAsset.getLiveAsset();
666-
return /** @type {AssertionOptionsToLiveAsset<T>} */ (liveAsset);
694+
return /** @type {AssetAssertionOptionsToLiveAsset<T>} */ (liveAsset);
667695
}
668696

669697
/**
@@ -895,12 +923,12 @@ export class AssetManager {
895923
* @template {GetLiveAssetFromUuidOrEmbeddedAssetDataOptions} T
896924
* @param {import("../../../src/mod.js").UuidString | object | null | undefined} uuidOrData
897925
* @param {T} options
898-
* @returns {Promise<AssertionOptionsToLiveAsset<T>?>}
926+
* @returns {Promise<AssetAssertionOptionsToLiveAsset<T>?>}
899927
*/
900928
async getLiveAssetFromUuidOrEmbeddedAssetData(uuidOrData, options) {
901929
const projectAsset = await this.getProjectAssetFromUuidOrEmbeddedAssetData(uuidOrData, options);
902930
if (!projectAsset) return null;
903931
const liveAsset = await projectAsset.getLiveAsset();
904-
return /** @type {AssertionOptionsToLiveAsset<T>} */ (liveAsset);
932+
return /** @type {AssetAssertionOptionsToLiveAsset<T>} */ (liveAsset);
905933
}
906934
}

editor/src/assets/liveAssetDataRecursionTracker/RecursionTracker.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,13 @@ export class RecursionTracker {
137137
/**
138138
* @template {GetLiveAssetDataOptions} [T = {}]
139139
* @param {import("../../../../src/util/mod.js").UuidString} uuid
140-
* @param {(liveAsset: import("../AssetManager.js").AssertionOptionsToLiveAsset<T>?) => void} cb
140+
* @param {(liveAsset: import("../AssetManager.js").AssetAssertionOptionsToLiveAsset<T>?) => void} cb
141141
* @param {T} options
142142
*/
143143
getLiveAsset(uuid, cb, options = /** @type {T} */ ({})) {
144144
this.getLiveAssetData(uuid, liveAssetData => {
145145
const liveAsset = liveAssetData?.liveAsset ?? null;
146-
const castLiveAsset = /** @type {import("../AssetManager.js").AssertionOptionsToLiveAsset<T>?} */ (liveAsset);
146+
const castLiveAsset = /** @type {import("../AssetManager.js").AssetAssertionOptionsToLiveAsset<T>?} */ (liveAsset);
147147
cb(castLiveAsset);
148148
}, options);
149149
}

editor/src/assets/projectAssetType/ProjectAssetTypeJavascript.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import {getEditorInstance} from "../../editorInstance.js";
88
* @property {boolean} useClosureCompiler
99
*/
1010

11-
// todo: better types for generics
1211
/**
13-
* @extends {ProjectAssetType<null, null, any, AssetBundleDiskDataProjectAssetTypeJavascriptAssetSettings>}
12+
* @extends {ProjectAssetType<null, null, string, AssetBundleDiskDataProjectAssetTypeJavascriptAssetSettings>}
1413
*/
1514
export class ProjectAssetTypeJavascript extends ProjectAssetType {
1615
static type = "renda:javascript";

editor/src/propertiesAssetContent/PropertiesAssetContentTask.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ export class PropertiesAssetContentTask extends PropertiesAssetContent {
2727
text: "Run Task",
2828
onClick: async () => {
2929
for (const asset of this.currentSelection) {
30-
const assetContent = await asset.readAssetData();
31-
this.editorInstance.taskManager.runTask({
32-
taskFileContent: assetContent,
33-
});
30+
this.editorInstance.taskManager.runTask(asset);
3431
}
3532
},
3633
},

editor/src/tasks/TaskManager.js

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,29 @@ import {getEditorInstance} from "../editorInstance.js";
22
import {autoRegisterTaskTypes} from "./autoRegisterTaskTypes.js";
33
import {Task} from "./task/Task.js";
44

5+
/**
6+
* @typedef {<T extends import("../assets/AssetManager.js").AssetAssertionOptions>(path: import("../util/fileSystems/EditorFileSystem.js").EditorFileSystemPath, assertionOptions: T) => Promise<import("../assets/AssetManager.js").AssetAssertionOptionsToReadAssetDataReturn<T>?>} ReadAssetFromPathSignature
7+
*/
8+
9+
/**
10+
* @typedef {<T extends import("../assets/AssetManager.js").AssetAssertionOptions>(uuid: import("../../../src/mod.js").UuidString, assertionOptions: T) => Promise<import("../assets/AssetManager.js").AssetAssertionOptionsToReadAssetDataReturn<T>?>} ReadAssetFromUuidSignature
11+
*/
12+
513
export class TaskManager {
614
/** @type {Map<string, typeof import("./task/Task.js").Task>} */
715
#registeredTasks = new Map();
816
/** @type {Map<string, Task>} */
917
#initializedTasks = new Map();
1018

19+
/** @typedef {import("../assets/ProjectAsset.js").ProjectAsset<import("../assets/projectAssetType/ProjectAssetTypeTask.js").ProjectAssetTypeTask>} TaskProjectAsset */
20+
21+
/**
22+
* This keeps track of which files were generated by which tasks.
23+
* This is to run child tasks in case a task has one of these files as a dependency.
24+
* @type {WeakMap<import("../assets/ProjectAsset.js").ProjectAssetAny, TaskProjectAsset>}
25+
*/
26+
#touchedTaskAssets = new WeakMap();
27+
1128
init() {
1229
for (const task of autoRegisterTaskTypes) {
1330
this.registerTaskType(task);
@@ -65,12 +82,64 @@ export class TaskManager {
6582

6683
/**
6784
* Runs a task with a specified configuration in a worker.
68-
* @param {Object} options
69-
* @param {import("../assets/projectAssetType/ProjectAssetTypeTask.js").TaskProjectAssetDiskData} options.taskFileContent
70-
* The content of the task file to run.
85+
* @param {TaskProjectAsset} taskAsset The task asset to run.
7186
*/
72-
async runTask({taskFileContent}) {
87+
async runTask(taskAsset) {
88+
const taskFileContent = await taskAsset.readAssetData();
7389
const taskType = this.initializeTask(taskFileContent.taskType);
74-
return await taskType.runTask(taskFileContent.taskConfig);
90+
const assetManager = getEditorInstance().projectManager.assetManager;
91+
92+
/**
93+
* @template T
94+
* @param {import("../assets/ProjectAsset.js").ProjectAsset<import("../assets/AssetManager.js").AssetAssertionOptionsToProjectAssetType<T>>?} asset
95+
*/
96+
const runDependencyTasksAndRead = async asset => {
97+
if (!asset) return null;
98+
const taskAsset = this.#touchedTaskAssets.get(asset);
99+
if (taskAsset) {
100+
await this.runTask(taskAsset);
101+
}
102+
const result = await asset?.readAssetData();
103+
return result || null;
104+
};
105+
106+
const result = await taskType.runTask({
107+
config: taskFileContent.taskConfig,
108+
needsAllGeneratedAssets: false,
109+
async readAssetFromPath(path, assertionOptions) {
110+
const asset = await assetManager?.getProjectAssetFromPath(path, {assertionOptions}) || null;
111+
return await runDependencyTasksAndRead(asset);
112+
},
113+
async readAssetFromUuid(uuid, assertionOptions) {
114+
const asset = await assetManager?.getProjectAssetFromUuid(uuid, assertionOptions) || null;
115+
return await runDependencyTasksAndRead(asset);
116+
},
117+
});
118+
119+
if (result.writeAssets) {
120+
for (const writeAssetData of result.writeAssets) {
121+
if (!assetManager) {
122+
throw new Error("Failed to write files from task, asset manager is not available.");
123+
}
124+
// TODO: Assert that the asset has the correct type. #67
125+
let asset = await assetManager.getProjectAssetFromPath(writeAssetData.path);
126+
if (!asset) {
127+
asset = await assetManager.registerAsset(writeAssetData.path, writeAssetData.assetType);
128+
}
129+
await asset.writeAssetData(/** @type {Object} **/ (writeAssetData.fileData));
130+
this.#touchedTaskAssets.set(asset, taskAsset);
131+
}
132+
}
133+
if (result.touchedAssets) {
134+
if (!assetManager) {
135+
throw new Error("Failed to register touched assets, asset manager is not available.");
136+
}
137+
for (const assetUuid of result.touchedAssets) {
138+
const asset = await assetManager.getProjectAssetFromUuid(assetUuid);
139+
if (asset) {
140+
this.#touchedTaskAssets.set(asset, taskAsset);
141+
}
142+
}
143+
}
75144
}
76145
}

editor/src/tasks/task/Task.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
1-
21
/**
32
* @typedef {new (...args: ConstructorParameters<typeof Task>) => Task} TaskConstructor
43
*/
54

5+
/**
6+
* @typedef RunTaskReturn
7+
* @property {RunTaskCreateAssetData[]} [writeAssets] A list of assets that this task should create when done running.
8+
* This is useful if you want to modify files in a very basic way. The assets will be created and written to the output location.
9+
* If the task is run programmatically, nothing is written and the program running the task can handle the result accordingly.
10+
* Note that if you are not writing some assets as a result of caching, but might write them in the future, you should add them to the `touchedAssets` list.
11+
* This way other tasks can trigger this task to run if it depends on them.
12+
* If you need more control over how assets are written, such as writing to a file stream, you can write them manually using
13+
* the current editor file system. But be sure to list the changed assets in `touchedAssets` as well. Though when using this
14+
* method, the task won't be able to be used programmatically. Unless you handle this case specifically when the `needsAlltouchedAssets`
15+
* flag is set to true.
16+
* @property {import("../../../../src/mod.js").UuidString[]} [touchedAssets] A list of assets that this task touched, or
17+
* might touch when the task is run a second time. This is used by other tasks for determining if this task needs to run before them.
18+
*/
19+
20+
/**
21+
* @typedef RunTaskCreateAssetData
22+
* @property {import("../../util/fileSystems/EditorFileSystem.js").EditorFileSystemPath} path
23+
* @property {string} assetType
24+
* @property {import("../../util/fileSystems/EditorFileSystem.js").AllowedWriteFileTypes} fileData
25+
*/
26+
27+
/**
28+
* @template TTaskConfig
29+
* @typedef RunTaskOptions
30+
* @property {TTaskConfig} config
31+
* @property {boolean} needsAllGeneratedAssets If true, running this task was triggered programmatically.
32+
* In this case the task should not write any assets to disk and return the changes in `writeAssets` instead.
33+
* @property {import("../TaskManager.js").ReadAssetFromPathSignature} readAssetFromPath Reads an asset from the file system.
34+
* If the asset was built by another task, the other task will run first in order to update the asset.
35+
* @property {import("../TaskManager.js").ReadAssetFromUuidSignature} readAssetFromUuid Reads an asset from the file system.
36+
* If the asset was built by another task, the other task will run first in order to update the asset.
37+
*/
38+
639
/**
740
* @template [TTaskConfig = unknown]
841
*/
@@ -58,10 +91,10 @@ export class Task {
5891
}
5992

6093
/**
61-
* @param {TTaskConfig} config
62-
* @returns {Promise<unknown>}
94+
* @param {RunTaskOptions<TTaskConfig>} options
95+
* @returns {Promise<RunTaskReturn>}
6396
*/
64-
async runTask(config) {
97+
async runTask(options) {
6598
throw new Error(`Task "${this.constructor.name}" does not implement runTask().`);
6699
}
67100
}

editor/src/tasks/task/TaskBundleAssets.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ export class TaskBundleAssets extends Task {
158158
}
159159

160160
/**
161-
* @param {TaskBundleAssetsConfig} config
161+
* @param {import("./Task.js").RunTaskOptions<TaskBundleAssetsConfig>} config
162162
*/
163-
async runTask(config) {
163+
async runTask({config}) {
164164
const fileSystem = this.editorInstance.projectManager.currentProjectFileSystem;
165165
if (!fileSystem) {
166166
throw new Error("Failed to create Bundle Scripts task: no project file system.");
@@ -197,5 +197,7 @@ export class TaskBundleAssets extends Task {
197197
this.#fileStreams.set(fileStreamId, bundleFileStream);
198198

199199
await this.#messenger.send("bundle", Array.from(assetUuids), fileStreamId);
200+
201+
return {};
200202
}
201203
}

0 commit comments

Comments
 (0)