@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
266 lines • 12 kB
JavaScript
import inquirer from "inquirer";
import ora from "ora";
import fs from "fs-extra";
import { promisify } from "node:util";
import path from "node:path";
import * as child from "node:child_process";
import { pathToFileURL } from "node:url";
const exec = promisify(child.exec);
function getTemplatesPath(templateType) {
// published bundle path exists if this is being run from the published geoprocessing package
// (e.g. via geoprocessing init or add:template)
const publishedBundlePath = path.join(import.meta.dirname, "..", "..", "templates", `${templateType}s`);
// console.log("import.meta.dirname", import.meta.dirname);
if (fs.existsSync(publishedBundlePath)) {
// console.log(
// "getTemplatesPath returning publishedBundlePath",
// publishedBundlePath
// );
// Use bundled templates if user running published version, e.g. via geoprocessing init
return publishedBundlePath;
}
else {
// console.log(
// "getTemplatesPath returning import.meta.dirname/../../..",
// path.join(import.meta.dirname, "..", "..", "..")
// );
// Use src templates
return path.join(import.meta.dirname, "..", "..", "..");
}
}
export async function getTemplateQuestion(templateType) {
const templatesPath = getTemplatesPath(templateType);
// Extract list of template names and descriptions from bundles
const templateNames = await fs.readdir(templatesPath);
if (!fs.existsSync(templatesPath)) {
throw new Error("Templates path does not exist: " + templatesPath);
}
if (templateNames.length === 0) {
console.error(`No add-on templates currently available`);
// console.log("template path:", templatesPath);
// console.log("add:template running from:", import.meta.dirname);
// console.log("CLI running from:", process.cwd());
process.exit();
}
const templateDescriptions = templateNames.map((name) => {
try {
const templatePackageMetaPath = path.join(templatesPath, name, "package.json");
return JSON.parse(fs.readFileSync(templatePackageMetaPath).toString())
.description;
}
catch (error) {
console.error(`Missing package.json or its description for template ${name}`);
console.error(error);
process.exit();
}
});
// Allow selection of one starter template or multiple add-on templates
const templateQuestion = {
type: templateType === "add-on-template" ? "checkbox" : "list",
name: "templates",
message: `What ${templateType}${templateType === "add-on-template" ? "s" : ""} would you like to install?`,
choices: templateNames.map((name, index) => ({
value: name,
name: `${name} - ${templateDescriptions[index]}`,
})),
};
return templateQuestion;
}
/**
* Asks template questions and invokes copy of templates to projectPath. Invoked via CLI so assume 'add-on-template' type
*/
async function addTemplate() {
// Assume invoked via CLI so we are installing add-on-templates
const templateType = "add-on-template";
const templateQuestion = await getTemplateQuestion(templateType);
const answers = await inquirer.prompt([templateQuestion]);
if (answers.templates) {
try {
const templateList = Array.isArray(answers.templates)
? answers.templates
: [answers.templates];
if (answers.templates.length > 0) {
await copyTemplates(templateType, templateList);
}
else {
return;
}
}
catch (error) {
console.error(error);
process.exit();
}
}
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
// module was not imported but called directly
await addTemplate();
}
/** Copies templates by name to gp project */
export async function copyTemplates(templateType, templateNames, options) {
const { interactive, projectPath, skipInstall } = {
interactive: true,
projectPath: ".",
skipInstall: false,
...options,
};
const spinner = interactive
? ora(`Adding templates`).start()
: {
start: () => false,
stop: () => false,
succeed: () => false,
fail: () => false,
};
if (!templateNames || templateNames.length === 0) {
spinner.succeed("No templates selected, skipping");
return;
}
if (!fs.existsSync(path.join(projectPath, "package.json"))) {
spinner.fail("Could not find your project, are you in your project root directory?");
process.exit();
}
const templatesPath = getTemplatesPath(templateType);
for (const templateName of templateNames) {
// Find template
const templatePath = path.join(templatesPath, templateName);
if (!fs.existsSync(templatePath)) {
spinner.fail(`Could not find template ${templateName} ${templatePath}`);
process.exit();
}
// Copy package metadata
spinner.start(`adding template ${templateName}`);
const templatePackage = JSON.parse(fs.readFileSync(`${templatePath}/package.json`).toString());
// Remove the templates seasketch dependency, the version will not match if running from canary gp release, and don't want it to overwrite
if (templatePackage.dependencies)
delete templatePackage.dependencies["@seasketch/geoprocessing"];
const projectPackage = JSON.parse(fs.readFileSync(`${projectPath}/package.json`).toString());
const packageJSON = {
...projectPackage,
dependencies: {
...projectPackage?.dependencies,
...templatePackage?.dependencies,
},
devDependencies: {
...projectPackage?.devDependencies,
...templatePackage?.devDependencies,
},
};
await fs.writeFile(path.join(projectPath, "package.json"), JSON.stringify(packageJSON, null, " "));
// Copy file assets, but only if there is something to copy. Creates directories first as needed
// Note that fs.copy will copy everything inside of the src directory, not the entire directory itself
try {
if (!fs.existsSync(path.join(projectPath, "src"))) {
fs.mkdirSync(path.join(projectPath, "src"));
}
if (fs.existsSync(path.join(templatePath, "src", "functions"))) {
if (!fs.existsSync(path.join(projectPath, "src", "functions"))) {
fs.mkdirSync(path.join(projectPath, "src", "functions"));
}
await fs.copy(path.join(templatePath, "src", "functions"), path.join(projectPath, "src", "functions"));
}
if (fs.existsSync(path.join(templatePath, "src", "components"))) {
if (!fs.existsSync(path.join(projectPath, "src", "components"))) {
fs.mkdirSync(path.join(projectPath, "src", "components"));
}
await fs.copy(path.join(templatePath, "src", "components"), path.join(projectPath, "src", "components"));
}
if (fs.existsSync(path.join(templatePath, "src", "assets"))) {
if (!fs.existsSync(path.join(projectPath, "src", "assets"))) {
fs.mkdirSync(path.join(projectPath, "src", "assets"));
}
await fs.copy(path.join(templatePath, "src", "assets"), path.join(projectPath, "src", "assets"));
}
if (fs.existsSync(path.join(templatePath, "src", "clients"))) {
if (!fs.existsSync(path.join(projectPath, "src", "clients"))) {
fs.mkdirSync(path.join(projectPath, "src", "clients"));
}
await fs.copy(path.join(templatePath, "src", "clients"), path.join(projectPath, "src", "clients"));
}
// Copy examples without test outputs, let the user generate them
if (fs.existsSync(path.join(templatePath, "examples"))) {
if (!fs.existsSync(path.join(projectPath, "examples"))) {
fs.mkdirSync(path.join(projectPath, "examples"));
}
if (fs.existsSync(path.join(templatePath, "examples", "sketches")) &&
!fs.existsSync(path.join(projectPath, "examples", "sketches"))) {
fs.mkdirSync(path.join(projectPath, "examples", "sketches"));
}
if (fs.existsSync(path.join(templatePath, "examples", "features")) &&
!fs.existsSync(path.join(projectPath, "examples", "features"))) {
fs.mkdirSync(path.join(projectPath, "examples", "features"));
}
}
if (fs.existsSync(path.join(templatePath, "_gitignore"))) {
// Convert to array of lines
const tplIgnoreArray = fs
.readFileSync(path.join(templatePath, "_gitignore"))
.toString()
.split("\n");
const commentIndex = tplIgnoreArray.findIndex((line) => line.startsWith("### Ignore"));
if (commentIndex === -1) {
throw new Error("Could not find separator in .gitignore file");
}
const startLine = commentIndex + 1;
if (tplIgnoreArray.length > startLine) {
// Merge back into string with newlines and append to end of project file
const tplIgnoreLines = tplIgnoreArray
.slice(startLine)
.reduce((acc, line) => {
return line.length > 0 ? acc.concat(line + "\n") : "";
}, "\n");
fs.appendFile(path.join(projectPath, ".gitignore"), tplIgnoreLines);
}
}
}
catch (error) {
spinner.fail("Error");
console.error(error);
process.exit();
}
// Merge geoprocessing metadata
// TODO: Should not duplicate existing entries
const tplGeoprocessing = JSON.parse(fs
.readFileSync(path.join(templatePath, "project", "geoprocessing.json"))
.toString());
const dstGeoprocessing = JSON.parse(fs
.readFileSync(path.join(projectPath, "project", "geoprocessing.json"))
.toString());
const geoprocessingJSON = {
...dstGeoprocessing,
clients: [
...(dstGeoprocessing?.clients || []),
...(tplGeoprocessing?.clients || []),
],
preprocessingFunctions: [
...(dstGeoprocessing?.preprocessingFunctions || []),
...(tplGeoprocessing?.preprocessingFunctions || []),
],
geoprocessingFunctions: [
...(dstGeoprocessing?.geoprocessingFunctions || []),
...(tplGeoprocessing?.geoprocessingFunctions || []),
],
};
fs.writeFileSync(path.join(projectPath, "project", "geoprocessing.json"), JSON.stringify(geoprocessingJSON, null, " "));
spinner.succeed(`added ${templateName}`);
}
// Install new dependencies
if (interactive && !skipInstall) {
spinner.start("installing new dependencies with npm");
try {
await exec("npm install", {
cwd: projectPath,
});
}
catch (error) {
if (error instanceof Error) {
console.log(error.message);
console.log(error.stack);
process.exit();
}
}
spinner.succeed("installed new dependencies");
}
}
export { addTemplate };
//# sourceMappingURL=addTemplate.js.map