@adonisjs/application
Version:
AdonisJS application class to read app related data
322 lines (316 loc) • 9.2 kB
JavaScript
import {
debug_default
} from "./chunk-QXFCAPYD.js";
// src/stubs/manager.ts
import { join as join2 } from "node:path";
import { cp } from "node:fs/promises";
import { RuntimeException as RuntimeException2, fsReadAll } from "@poppinss/utils";
// src/stubs/stub.ts
import * as tempura from "tempura";
import string from "@poppinss/utils/string";
import { dirname, isAbsolute } from "node:path";
import { RuntimeException } from "@poppinss/utils";
import { mkdir, writeFile } from "node:fs/promises";
import stringHelpers from "@poppinss/utils/string";
import StringBuilder from "@poppinss/utils/string_builder";
// src/helpers.ts
import { join } from "node:path";
import { access, readFile } from "node:fs/promises";
async function readFileFromSources(fileName, sources) {
for (let source of sources) {
const filePath = join(source, fileName);
const contents = await readFileOptional(filePath);
if (contents !== null) {
return {
contents,
filePath,
fileName,
source
};
}
}
return null;
}
async function readFileOptional(filePath) {
try {
return await readFile(filePath, "utf-8");
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
return null;
}
}
async function pathExists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
function parseStubExports(contents) {
const chunks = contents.split(/\r\n|\n/);
const body = [];
const exportedBlocks = [];
chunks.forEach((line) => {
if (line.includes("<!--EXPORT_START-->")) {
let [inital, rest] = line.split("<!--EXPORT_START-->");
let [exports, remaining] = rest.split("<!--EXPORT_END-->");
inital = inital.trim();
remaining = remaining.trim();
const remainingContents = inital && remaining ? `${inital}
${remaining}` : inital || remaining || "";
exportedBlocks.push(exports);
if (remainingContents) {
body.push(remainingContents);
}
} else {
body.push(line);
}
});
const attributes = exportedBlocks.reduce(
(result, block) => {
Object.assign(result, JSON.parse(block));
return result;
},
{}
);
return { attributes, body: body.join("\n") };
}
// src/stubs/stub.ts
function stubStringBuilder(value) {
return new StringBuilder(value);
}
Object.assign(stubStringBuilder, stringHelpers);
var Stub = class {
/**
* The absolute path to the stub file. Need it for reporting
* errors
*/
#stubPath;
/**
* The contents of the stub to process
*/
#stubContents;
/**
* Application class reference
*/
#app;
constructor(app, stubContents, stubPath) {
this.#app = app;
this.#stubPath = stubPath;
this.#stubContents = stubContents;
}
/**
* Patch error stack and point it to the stub file
*/
#patchErrorStack(error) {
const stack = error.stack.split("\n");
stack.splice(1, 0, ` at anonymous (${this.#stubPath}:0:0)`);
error.stack = stack.join("\n");
}
/**
* Patch tempura error stack and point it to the stub file
*/
#patchTempuraStack(error) {
const stack = error.stack.split("\n");
const templateErrorLine = stack[1].match(/<anonymous>:(\d+):\d+\)$/);
if (!templateErrorLine) {
stack.splice(1, 0, ` at anonymous (${this.#stubPath}:0:0)`);
} else {
stack.splice(1, 0, ` at anonymous (${this.#stubPath}:${templateErrorLine[1]}:0)`);
}
error.stack = stack.join("\n");
}
/**
* Validates the "to" attribute
*/
#validateToAttribute(attributes) {
if (!attributes.to) {
const error = new RuntimeException(`Missing "to" attribute in stub exports`);
throw error;
}
if (!isAbsolute(attributes.to)) {
const error = new RuntimeException(
`The value for "to" attribute must be an absolute file path`
);
throw error;
}
}
/**
* Returns the default state for the stub
*/
#getStubDefaults() {
return {
app: this.#app,
randomString: string.random,
generators: this.#app.generators,
exports: (value) => {
return `<!--EXPORT_START-->${JSON.stringify(value)}<!--EXPORT_END-->`;
},
string: stubStringBuilder
};
}
/**
* Renders stub using tempura templating syntax.
*/
async #renderStub(data) {
try {
const render = tempura.compile(this.#stubContents, {
props: Object.keys(data)
});
return render(data).trim();
} catch (error) {
this.#patchTempuraStack(error);
throw error;
}
}
/**
* Parsers the stub exports
*/
#parseExports(stubOutput) {
try {
const { body, attributes } = parseStubExports(stubOutput);
this.#validateToAttribute(attributes);
return { attributes, body };
} catch (error) {
this.#patchErrorStack(error);
throw error;
}
}
/**
* Prepare stub to be written to the disk
*/
async prepare(stubData) {
const data = {
...this.#getStubDefaults(),
...stubData
};
const { attributes, body } = this.#parseExports(await this.#renderStub(data));
debug_default("prepared stub %s", body);
debug_default("stub attributes %O", attributes);
return {
contents: body,
destination: attributes.to,
force: stubData.force !== void 0 ? stubData.force : !!attributes.force,
attributes
};
}
/**
* Generate resource for the stub. Writes file to the disk
*/
async generate(stubData) {
const { force, ...stub } = await this.prepare(stubData);
const hasFile = await pathExists(stub.destination);
const directory = dirname(stub.destination);
if (!hasFile) {
debug_default("writing file to %s", stub.destination);
await mkdir(directory, { recursive: true });
await writeFile(stub.destination, stub.contents);
return {
status: "created",
skipReason: null,
...stub
};
}
if (hasFile && force) {
debug_default("overwriting file to %s", stub.destination);
await mkdir(directory, { recursive: true });
await writeFile(stub.destination, stub.contents);
return {
status: "force_created",
skipReason: null,
...stub
};
}
return {
status: "skipped",
skipReason: "File already exists",
...stub
};
}
};
// src/stubs/manager.ts
var StubsManager = class {
#app;
/**
* Absolute path to the directory where stubs should
* be published or read from with priority
*/
#publishTarget;
constructor(app, publishTarget) {
this.#app = app;
this.#publishTarget = publishTarget;
}
/**
* Returns the path to the stubs source directory of a package
*/
async #getPackageSource(packageName) {
const pkgMainExports = await this.#app.import(packageName);
if (!pkgMainExports.stubsRoot) {
throw new RuntimeException2(
`Cannot resolve stubs from package "${packageName}". Make sure the package entrypoint exports "stubsRoot" variable`
);
}
return pkgMainExports.stubsRoot;
}
/**
* Creates an instance of stub by its name. The lookup is performed inside
* the publishTarget and the optional source or pkg destination.
*/
async build(stubName, options) {
const sources = [this.#publishTarget];
if (options?.source) {
sources.push(options.source);
}
if (options?.pkg) {
sources.push(await this.#getPackageSource(options.pkg));
}
debug_default('finding stub "%s" in sources "%O"', stubName, sources);
const file = await readFileFromSources(stubName, sources);
if (!file) {
throw new RuntimeException2(`Unable to find stub "${stubName}"`, {
cause: `Scanned locations:
${sources.join("\n")}`
});
}
debug_default('building stub "%s"', file.filePath);
return new Stub(this.#app, file.contents, file.filePath);
}
/**
* Copy one or more stub files from a custom location to publish
* target.
*/
async copy(stubPath, options) {
const filesCopied = [];
const copyOptions = {
recursive: true,
force: options.overwrite === true ? true : false
};
const source = "source" in options ? join2(options.source, stubPath) : join2(await this.#getPackageSource(options.pkg), stubPath);
try {
const files = await fsReadAll(source, {
filter: (path) => path === "" || path.endsWith(".stub")
});
debug_default('copying stubs from "%s" with options %O', source, copyOptions);
debug_default('preparing to copy stubs "%s"', files);
for (let filePath of files) {
const sourcePath = join2(source, filePath);
const destinationPath = join2(this.#publishTarget, stubPath, filePath);
await cp(sourcePath, destinationPath, copyOptions);
filesCopied.push(destinationPath);
}
return filesCopied;
} catch (error) {
if (error.code === "ENOENT") {
const readingSource = "source" in options ? options.source : options.pkg;
throw new Error(`Cannot find "${stubPath}" stub in "${readingSource}" destination`);
}
throw error;
}
}
};
export {
StubsManager
};