All files / core/blong-gogo/src kopi.ts

0% Statements 0/102
0% Branches 0/1
0% Functions 0/1
0% Lines 0/102

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103                                                                                                                                                                                                             
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
import {basename, dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import {listTemplateFiles} from './template-files.ts';

export interface CreateRealmOptions {
    /**
     * Entity name — the "object" of the `subjectObjectPredicate` triple.
     * Defaults to `entry`. Substituted for the `$object` / `$Object` template
     * tokens (in both file paths and contents).
     */
    object?: string;
}

const capitalize = (value: string): string => value.charAt(0).toUpperCase() + value.slice(1);

/**
 * Locate the scaffolding template root.
 *
 * The template files live in the `@feasibleone/blong-kopi` package. In the
 * monorepo that is the sibling `core/blong-kopi`; in the published `blong`
 * package the template is bundled into `./template` by the `prepublishOnly`
 * script (see `scripts/copy-template.mjs`) — the copy happens ONLY on npm
 * publish, so the `template/` folder is git-ignored and never committed.
 *
 * Resolution order:
 *  1. dev sibling: `<this-package>/../../blong-kopi/package.json`
 *  2. publish bundle: `<this-package>/../../template/package.json`
 */
function resolveTemplateRoot(): string {
    const here = fileURLToPath(new URL('..', import.meta.url)); // core/blong-gogo/
    for (const candidate of ['../blong-kopi', './template']) {
        const root = join(here, candidate);
        if (existsSync(join(root, 'package.json'))) return root;
    }
    throw new Error(
        'blong scaffolding template not found (expected a sibling `blong-kopi` package ' +
            'or a bundled `template/` generated by the publish step)',
    );
}

/**
 * Scaffold a new realm from the `@feasibleone/blong-kopi` template.
 *
 * Copies the whole template into `destUrl` (the realm folder), substituting:
 *  - `$subject` → the destination folder basename (the realm name)
 *  - `$Subject` → capitalized realm name
 *  - `$object`  → the entity name (default `entry`)
 *  - `$Object`  → capitalized entity name
 *
 * Every generated `.ts` file is prefixed with an `import unchanged ...` marker;
 * on re-scaffold, files that do NOT start with that marker are treated as hand
 * edits and left alone (idempotent).
 *
 * Used by both the runtime auto-trigger (`blong-gogo/src/load.ts`) and the
 * explicit `blong realm <name>` CLI path (`blong-gogo/bin/blong.ts`).
 */
export async function createRealm(
    destUrl: string,
    logger?: {warn?: (message: string) => void},
    options: CreateRealmOptions = {},
): Promise<string[]> {
    const result = [];
    const cwd = resolveTemplateRoot();
    destUrl = destUrl.startsWith('file://') ? dirname(destUrl.slice(7)) : destUrl;
    const subject = basename(destUrl);
    const object = options.object ?? 'entry';
    const replace = (str: string): string =>
        str
            .replaceAll('$subject', subject)
            .replaceAll('$Subject', capitalize(subject))
            .replaceAll('$object', object)
            .replaceAll('$Object', capitalize(object));
    logger?.warn?.(`Creating realm ${destUrl} from ${cwd} (entity: ${object})`);
    for (const file of listTemplateFiles(cwd, {
        extraIgnore: [
            'package.json', // written separately (name substituted below)
            // The scaffolder itself and template-specific docs are not scaffolded.
            'kopi.ts',
            'README.md',
            'CHANGELOG.md',
        ],
    })) {
        const [source, dest] = [join(cwd, file), join(destUrl, replace(file))];
        if (!existsSync(dest) || readFileSync(dest, 'utf8').startsWith('import unchanged')) {
            mkdirSync(dirname(dest), {recursive: true});
            const content = readFileSync(source, 'utf8');
            writeFileSync(
                dest,
                file.endsWith('.ts')
                    ? "import unchanged from '@feasibleone/blong';\r" + replace(content)
                    : replace(content),
            );
            result.push(dest);
        }
    }
    writeFileSync(
        join(destUrl, 'package.json'),
        readFileSync(join(cwd, 'package.json'), 'utf8').replace('@feasibleone/blong-kopi', subject),
    );
    return result;
}