@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
147 lines • 7.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const schema_1 = require("@sprucelabs/schema");
class StaticToInstanceTestFileMigratorImpl {
static Class;
static Migrator() {
return new (this.Class ?? this)();
}
migrate(contents) {
(0, schema_1.assertOptions)({ contents }, ['contents']);
// 1a. Remove `static ` only when it appears immediately before a method
// that has the `@test()` decorator
// 1b. If the contents include `export default abstract class`,
// remove `static` from all methods
const isAbstractTest = /export\s+default\s+(?:abstract\s+class\b|class\s+(Abstract\w*))/m.test(contents);
let cleanedUp = isAbstractTest
? contents.replaceAll(' static ', ' ')
: contents.replace(
// Matches @test() or @seed(...) followed (on next line) by optional visibility and `static`.
/(@(?:(?:test|seed)\([\s\S]*?\))\s*\n\s*(?:public|protected)\s+)static\s+/g, '$1');
// 2. Add `@suite()` above `export default class` if it's not already present
if (!isAbstractTest && !cleanedUp.includes('@suite')) {
cleanedUp = cleanedUp.replace(/export default class/, '@suite()\nexport default class');
}
// 3. Ensure `suite` is imported from `@sprucelabs/test-utils`
if (!this.hasSuiteImport(cleanedUp)) {
if (cleanedUp.includes('{ test')) {
cleanedUp = cleanedUp.replace('{ test', '{ test, suite');
}
else if (cleanedUp.includes('test }')) {
cleanedUp = cleanedUp.replace('test }', 'test, suite }');
}
else {
cleanedUp = cleanedUp.replace('test,', 'test,\n suite,');
}
}
const thisCallNames = this.findThisCalls(cleanedUp);
for (const name of thisCallNames) {
cleanedUp = this.removeStaticFromDeclaration(cleanedUp, name);
}
// 4. lifecicle methods
const methods = ['beforeEach', 'afterEach'];
for (const method of methods) {
cleanedUp = cleanedUp.replace(`static async ${method}()`, `async ${method}()`);
}
cleanedUp = this.fixNonNullAssertions(cleanedUp);
cleanedUp = cleanedUp.replaceAll('= >', '=>');
cleanedUp = cleanedUp.replaceAll('! =', ' =');
return cleanedUp;
}
hasSuiteImport(text) {
const pattern = new RegExp(`import\\s+(?:[\\s\\S]*?\\bsuite\\b[\\s\\S]*?)\\s+from\\s+['"]@sprucelabs/test-utils['"]`);
return pattern.test(text);
}
findThisCalls(contents) {
// Matches either `this.myProp` or `delete this.myProp`
// if followed by space, punctuation, parentheses, or end of string
const thisPropertyRegex = /(?:delete\s+)?this\.(\w+)(?=[\s.(),;]|$)/g;
const names = [];
let match;
while ((match = thisPropertyRegex.exec(contents)) !== null) {
const propName = match[1];
if (!names.includes(propName)) {
names.push(propName);
}
}
return names;
}
removeStaticFromDeclaration(contents, name) {
/**
* 1) Remove `static` for methods/getters/setters
*/
const methodPattern = new RegExp(`((?:public|protected|private)?\\s+)?` + // group 1: optional visibility + space
`static\\s+` + // literal 'static '
`(?:(async)\\s+)?` + // group 2: 'async'?
`(?:(get|set)\\s+)?` + // group 3: 'get' or 'set'?
`(${name})\\s*\\(`, // group 4: the identifier + '('
'g');
let updated = contents.replace(methodPattern, (match, g1, g2, g3, g4) => {
const asyncPart = g2 ? g2 + ' ' : '';
const accessorPart = g3 ? g3 + ' ' : '';
// Rebuild the declaration without "static"
return `${g1 ?? ''}${asyncPart}${accessorPart}${g4}(`;
});
/**
* 2) Remove `static` from property declarations, including those marked `readonly`.
* e.g.
* private static myProp => private myProp!
* private static readonly myProp => private readonly myProp!
* private static myProp?: Type => private myProp?: Type
*/
const propertyPattern = new RegExp(`((?:public|protected|private)?\\s+)?` + // group 1: optional visibility + space
`static\\s+` + // literal "static "
`(readonly\\s+)?` + // group 2: optional "readonly " (with trailing space)
`(${name})(\\?)?` + // group 3: property name, group 4: optional "?"
`(?=[\\s=:\\[;]|$)`, // lookahead
'g');
updated = updated.replace(propertyPattern, (match, g1, g2, g3, g4) => {
// g1 => "private ", "protected ", or "public " (plus any spacing) or undefined
// g2 => "readonly " if present, else undefined
// g3 => property name (e.g. 'test')
// g4 => "?" if property is optional, else undefined
// If it's optional, keep the '?' or else add '!'
// (Adjust if you'd rather remove the '!' in this step. Currently we're adding it if not optional.)
const optionalChar = g4 ? g4 : '!';
// Rebuild, dropping 'static' but retaining visibility + optional "readonly " + name + '?' or '!'
return `${g1 ?? ''}${g2 ?? ''}${g3}${optionalChar}`;
});
return updated;
}
fixNonNullAssertions(contents) {
const lines = contents.split('\n');
const propertyRegex = /^(\s*)(public|protected|private)(\s+readonly)?\s+(\w+)\s*(!)?\s*:\s*([^=;]+)(=.*)?;?$/;
const updatedLines = lines.map((originalLine) => {
// Skip lines containing "static"
if (originalLine.includes('static')) {
return originalLine;
}
const match = originalLine.match(propertyRegex);
if (!match) {
return originalLine;
}
let [, leadingWhitespace, visibility, readonlyPart = '', propName, exclamation, typeDecl, assignment,] = match;
// Trim trailing whitespace from the type
typeDecl = typeDecl.trim();
if (assignment) {
// Remove the bang if there's an assignment
exclamation = '';
// Remove trailing semicolon
assignment = assignment.replace(/;$/, '');
// Ensure we always have " = " at the start
// E.g. "=something" => " = something"
assignment = assignment.replace(/^=\s*/, ' = ');
}
else {
// No assignment? Add bang
exclamation = '!';
}
// Rebuild line, preserving leading indentation
const rebuilt = `${leadingWhitespace}${visibility}${readonlyPart} ${propName}${exclamation}: ${typeDecl}${assignment ?? ''}`;
return rebuilt;
});
return updatedLines.join('\n');
}
}
exports.default = StaticToInstanceTestFileMigratorImpl;
//# sourceMappingURL=StaticToInstanceTestFileMigrator.js.map