mascot-app
Version:
Master ActionScript COnfigurator Tool. MASCOT automatically resolves ActionScript project dependencies and generates `asconfig.json` files for easy compilation with `asconfigc`.
420 lines (371 loc) • 14.1 kB
JavaScript
const fs = require("fs");
const path = require("path");
const { deepMergeData } = require("cli-primer");
/**
* Generates `asconfig.json` files for projects listed in `projects.json`.
* Outputs one `asconfig.json` file in the root of each project directory.
*
* @param {string} workspaceDir
* Absolute path to the folder where repositories are cloned.
*
* @param {string} cacheDir
* Absolute path to the folder where `projects.json`, `classes.json`, and `deps.json`
* are stored.
*
* @param {boolean} [overwrite=false]
* Whether to overwrite existing `asconfig.json` files.
*
* @param {Object|null} [defaults=null]
* Optional. Object containing one or more of the following keys: `config_type`,
* `copy_assets`, `bin_dir`, `src_dir`. If not given, the following are assumed:
* {
* config_type: "air",
* copy_assets: true,
* bin_dir: "bin",
* src_dir: "src",
* }
*
* @param {Object|null} [base = null]
* Optional. Object containing a base/blueprint version of `asconfig` data to use.
* Any build-related settings in-here will be overwritten, but everything else will
* be kept verbatim (e.g., packaging settings, or specific compiler flags, e.g.,
* `advanced-telemetry`).
*/
function writeConfig(
workspaceDir,
cacheDir,
overwrite = false,
defaults = null,
base = null
) {
const projectsFilePath = path.join(cacheDir, "projects.json");
const classesFilePath = path.join(cacheDir, "classes.json");
const depsFilePath = path.join(cacheDir, "deps.json");
const problemsFilePath = path.join(cacheDir, "problems.log");
// Ensure required files exist
if (
!fs.existsSync(projectsFilePath) ||
!fs.existsSync(classesFilePath) ||
!fs.existsSync(depsFilePath)
) {
const errorMsg =
"At least one of the required files `projects.json`, `classes.json`, or `deps.json` is missing.";
console.error(errorMsg);
fs.appendFileSync(problemsFilePath, errorMsg + "\n\n");
return;
}
// Load data
const projects = JSON.parse(fs.readFileSync(projectsFilePath));
const classes = JSON.parse(fs.readFileSync(classesFilePath));
const deps = JSON.parse(fs.readFileSync(depsFilePath));
// Apply defaults
const defaultValues = {
config_type: "air",
copy_assets: true,
bin_dir: "bin",
src_dir: "src",
...defaults,
};
const problems = [];
// Helper to sanitize file names
function sanitizeFileName(name) {
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
}
// Write `asconfig.json` for each project
projects.forEach((project) => {
const {
project_home_dir,
classFiles,
has_lib_dir,
project_name,
is_app_probability,
} = project;
const projectPath = path.resolve(project_home_dir);
const configFilePath = path.join(projectPath, "asconfig.json");
// Skip if file exists and overwrite is false
if (!overwrite && fs.existsSync(configFilePath)) {
console.log(`Skipping existing config for project: ${projectPath}`);
return;
}
// Determine project type
const projectType = is_app_probability >= 0.5 ? "app" : "lib";
// Determine main class
const projectDeps =
deps.find((dep) => dep.project_path === projectPath) || {};
const { root_classes = [], project_dependencies = [] } = projectDeps;
const mainClass =
projectType === "app"
? root_classes.length > 0
? path.parse(root_classes[0].file_path).name
: "Main"
: undefined;
const appDescriptor =
projectType === "app"
? root_classes.length > 0
? root_classes[0].descriptor_file_path
: undefined
: undefined;
// Resolve library paths
const libraryPath = [
...(has_lib_dir ? ["lib"] : []),
...project_dependencies.map((depPath) =>
path.join(depPath, defaultValues.bin_dir)
),
];
// Build `asconfig.json` structure
const asConfigInherited = base || {};
const asConfigOwn = {
config: defaultValues.config_type,
...(projectType === "app"
? { type: "app", mainClass, application: appDescriptor }
: { type: "lib" }),
copySourcePathAssets: defaultValues.copy_assets,
compilerOptions: {
debug: defaultValues.debug_mode,
"library-path": libraryPath,
output:
projectType === "lib"
? `${defaultValues.bin_dir}/${sanitizeFileName(project_name)}.swc`
: `${defaultValues.bin_dir}/${mainClass}.swf`,
...(projectType === "lib"
? { "include-sources": [defaultValues.src_dir] }
: {}),
"source-path": [defaultValues.src_dir],
},
};
const asConfig = deepMergeData(asConfigInherited, asConfigOwn);
// Write `asconfig.json`
try {
fs.writeFileSync(configFilePath, JSON.stringify(asConfig, null, 2));
console.log(`Generated asconfig.json for project: ${projectPath}`);
} catch (err) {
const errorMsg = `Failed to write asconfig.json for project: ${projectPath}. Error: ${err.message}`;
console.error(errorMsg);
problems.push(errorMsg);
}
});
// Append problems to `problems.log`
if (problems.length > 0) {
fs.appendFileSync(problemsFilePath, problems.join("\n") + "\n\n");
}
console.log("Configuration generation complete.");
}
/**
* Writes or updates `.vscode/settings.json` files in each project listed in `projects.json`.
*
* @param {string} workspaceDir - Absolute path to the folder where repositories are cloned.
* @param {string} cacheDir - Absolute path to the folder where `projects.json` is stored.
* @param {Object} settings - Key-value pairs to add to the `settings.json` file.
* The "$sdk" short key will be expanded to "as3mxml.sdk.framework".
* @param {boolean} [purge=false] - Whether to replace the `settings.json` file entirely.
*/
function writeVSCSettings(workspaceDir, cacheDir, settings, purge = false) {
const projectsFilePath = path.join(cacheDir, "projects.json");
const problemsFilePath = path.join(cacheDir, "problems.log");
// Ensure `projects.json` exists
if (!fs.existsSync(projectsFilePath)) {
const errorMsg =
"`projects.json` is missing. Cannot write VSCode settings.";
console.error(errorMsg);
fs.appendFileSync(problemsFilePath, errorMsg + "\n");
return;
}
// Exit early if no settings are provided
if (!settings || Object.keys(settings).length === 0) {
console.log("No settings provided. Exiting early.");
return;
}
// Expand "$sdk" to "as3mxml.sdk.framework"
if (settings["$sdk"]) {
settings["as3mxml.sdk.framework"] = settings["$sdk"];
delete settings["$sdk"];
}
const projects = JSON.parse(fs.readFileSync(projectsFilePath));
const problems = [];
projects.forEach((project) => {
const { project_home_dir } = project;
const projectPath = path.resolve(project_home_dir);
const vscodePath = path.join(projectPath, ".vscode");
const settingsPath = path.join(vscodePath, "settings.json");
try {
let existingSettings = {};
// Handle purge
if (purge && fs.existsSync(settingsPath)) {
fs.unlinkSync(settingsPath);
console.log(`Purged existing settings for project: ${projectPath}`);
}
// Ensure `.vscode` directory exists
if (!fs.existsSync(vscodePath)) {
fs.mkdirSync(vscodePath);
}
// Load existing settings if not purging
if (!purge && fs.existsSync(settingsPath)) {
try {
existingSettings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
} catch (err) {
problems.push(
`Failed to parse existing settings for project: ${projectPath}. Error: ${err.message}`
);
}
}
// Merge new settings
const updatedSettings = {
...existingSettings,
...settings,
};
// Write updated settings
fs.writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2));
console.log(`Updated settings for project: ${projectPath}`);
} catch (err) {
const errorMsg = `Failed to write settings for project: ${projectPath}. Error: ${err.message}`;
console.error(errorMsg);
problems.push(errorMsg);
}
});
// Append problems to `problems.log`
if (problems.length > 0) {
fs.appendFileSync(problemsFilePath, problems.join("\n") + "\n\n");
}
console.log("VSCode settings update complete.");
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////
////////////////////////////////////
//////////////////////////
////////////////
//////////
/**
* Writes custom build tasks to the `tasks.json` file in the `.vscode` folder for each project.
*
* @param {string} workspaceDir - Absolute path to the folder where repositories are cloned.
* @param {string} cacheDir - Absolute path to the cache directory where `tasks.json` is located.
* @param {Object} settings - Object containing essential information for the tasks.
* `path_to_asconfigc` and `path_to_air_sdk` are expected keys.
* @param {boolean} [purge=false] - Whether to replace existing MASCOT tasks or skip if found.
*/
function writeVSCTasks(workspaceDir, cacheDir, settings, purge = false) {
const tasksFilePath = path.join(cacheDir, "tasks.json");
const problemsFilePath = path.join(cacheDir, "problems.log");
// Ensure `tasks.json` exists
if (!fs.existsSync(tasksFilePath)) {
const errorMsg = "`tasks.json` is missing. Cannot write VSCode tasks.";
console.error(errorMsg);
fs.appendFileSync(problemsFilePath, errorMsg + "\n");
return;
}
// Validate settings
const pathToAsconfigc = settings.path_to_asconfigc || "asconfigc";
const pathToAirSdk = settings.path_to_air_sdk;
if (!pathToAirSdk) {
const errorMsg = "`path_to_air_sdk` is required in settings.";
console.error(errorMsg);
fs.appendFileSync(problemsFilePath, errorMsg + "\n");
return;
}
const tasks = JSON.parse(fs.readFileSync(tasksFilePath));
const problems = [];
tasks.forEach((task) => {
const { project_path, project_build_tasks } = task;
const vscodePath = path.join(project_path, ".vscode");
const vscodeTasksPath = path.join(vscodePath, "tasks.json");
try {
// Ensure `.vscode` folder exists
if (!fs.existsSync(vscodePath)) {
fs.mkdirSync(vscodePath);
}
// Load or initialize tasks.json
let tasksJson = { version: "2.0.0", tasks: [] };
if (fs.existsSync(vscodeTasksPath)) {
tasksJson = JSON.parse(fs.readFileSync(vscodeTasksPath, "utf-8"));
}
// Filter out existing MASCOT tasks
if (purge) {
tasksJson.tasks = tasksJson.tasks.filter(
(t) => !t.label.startsWith("MASCOT: ")
);
} else if (tasksJson.tasks.some((t) => t.label.startsWith("MASCOT: "))) {
console.log(
`Skipping existing MASCOT tasks for project: ${project_path}`
);
return;
}
// Nested helper function to create one sub-task.
function createSubTask(
$depProjectPath,
$index,
$pathToAsconfigc,
$pathToAirSdk,
$debug,
$previousTaskLabel = null
) {
const label = `MASCOT: build dependency #${$index + 1} (${
$debug ? "debug" : "release"
})`;
tasksJson.tasks.push({
label,
type: "shell",
command: $pathToAsconfigc,
args: [
"--sdk",
$pathToAirSdk,
"--project",
$depProjectPath,
`--debug=${$debug}`,
],
group: { kind: "none", isDefault: false },
problemMatcher: [],
...($previousTaskLabel ? { dependsOn: $previousTaskLabel } : {}),
});
return label;
}
const isRebuild = project_build_tasks.length === 0;
project_build_tasks.pop(); // ignore the master task
// Add tasks both for debug and release builds
[true, false].forEach((debugMode) => {
// Add a sub-task for each dependency
let previousTaskLabel = null;
project_build_tasks.forEach((depProjectPath, index) => {
previousTaskLabel = createSubTask(
depProjectPath,
index,
pathToAsconfigc,
pathToAirSdk,
debugMode,
previousTaskLabel
);
});
// Add a master task for compiling the project itself.
tasksJson.tasks.push({
label: `MASCOT: compile ${debugMode ? "debug" : "release"}${
project_build_tasks.length
? " (with deps)"
: isRebuild
? " (not needed)"
: ""
}`,
type: "actionscript",
debug: debugMode,
asconfig: "asconfig.json",
group: "build",
problemMatcher: [],
...(previousTaskLabel ? { dependsOn: previousTaskLabel } : {}),
});
});
// Write updated tasks.json
fs.writeFileSync(vscodeTasksPath, JSON.stringify(tasksJson, null, 2));
console.log(`Updated tasks.json for project: ${project_path}`);
} catch (err) {
const errorMsg = `Failed to write tasks.json for project: ${project_path}. Error: ${err.message}`;
console.error(errorMsg);
problems.push(errorMsg);
}
});
// Append problems to `problems.log`
if (problems.length > 0) {
fs.appendFileSync(problemsFilePath, problems.join("\n") + "\n");
}
console.log("VSCode tasks generation complete.");
}
module.exports = { writeConfig, writeVSCSettings, writeVSCTasks };