isomorphic-typescript / ts-monorepo

A tool for managing TypeScript projects with multiple packages.
15 stars 4 forks source link

Question: Can packageRoot be the root of the repository? #1

Open hcharley opened 4 years ago

hcharley commented 4 years ago

It seems like these values are not allowed ".", "/", false. And "~" did not alias to the root.

hcharley commented 4 years ago

Just for reference, I hacked the source code to remove packageRoot requirement:

node_modules/@isomorphic-typescript/ts-monorepo/distribution/sync-logic/sync-packages.js#L1-L207
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const ansicolor = require("ansicolor");
const path = require("path");
const comment_json_1 = require("comment-json");
// Utils
const fs_async_1 = require("../util/fs-async");
const log_1 = require("../util/log");
const validate_presence_in_file_system_1 = require("../util/validate-presence-in-file-system");
// Config File Validation
const config_validator_1 = require("../config-file-structural-checking/config.validator");
const validateNpmPackageName = require("validate-npm-package-name");
const package_dependency_tracker_1 = require("./package-dependency-tracker");
const sync_package_json_1 = require("./sync-package.json");
const sync_tsconfig_json_1 = require("./sync-tsconfig.json");
const sync_lerna_json_1 = require("./sync-lerna.json");
const sync_tsconfig_leaves_json_1 = require("./sync-tsconfig-leaves.json");
const command_runner_1 = require("../util/command-runner");
const recursive_delete_directory_1 = require("../util/recursive-delete-directory");
const CONFIGURATION_ERROR = new Error("See above error message(s)");
CONFIGURATION_ERROR.stack = undefined;
CONFIGURATION_ERROR.name = "ConfigurationError";
function syncPackages(configFileRelativePath, configAbsolutePath) {
    return __awaiter(this, void 0, void 0, function* () {
        package_dependency_tracker_1.PackageDependencyTracker.reset();
        const configFilePresence = yield validate_presence_in_file_system_1.validateFilePresence(configAbsolutePath, undefined, configFileRelativePath);
        if (!configFilePresence.exists) {
            const fileDoesntExists = new Error(`The config file ${ansicolor.green(configFileRelativePath)} could not be found`);
            fileDoesntExists.name = "Config file not found";
            throw fileDoesntExists;
        }
        else if (configFilePresence.wrong) {
            throw CONFIGURATION_ERROR;
        }
        // Read config file.
        const configFileContents = (yield fs_async_1.fsAsync.readFile(configAbsolutePath)).toString();
        // TODO: pass comments down to generated jsons.
        // parse as json
        var parsedJson;
        try {
            parsedJson = comment_json_1.parse(configFileContents, undefined, true);
        }
        catch (e) {
            if (e.name === "SyntaxError") {
                e.message = `\n ${ansicolor.magenta('subject:')} ${ansicolor.green("ts-monorepo.json")}${"\n"}   ${ansicolor.red("error")}: ${e.message} on line ${ansicolor.green(e.line)}, column ${ansicolor.green("" + (e.column + 1))}\n`;
                e.stack = undefined;
            }
            e.name = "ts-monorepo.json parse error from library " + ansicolor.cyan("comment-json");
            throw e;
        }
        // validate the json
        var configFileJSON;
        try {
            configFileJSON = config_validator_1.default(parsedJson);
        }
        catch (e) {
            const error = e;
            const tempCharStart = "→";
            error.name = "invalid ts-monorepo.json";
            function validationErrorFix(input) {
                const lines = (input
                    .replace(new RegExp(`${error.name}: `, "g"), "") // remove error name from beginning of message
                    .replace(/TSMonorepoConfig/g, "\n" + tempCharStart + ansicolor.green("ts-monorepo.json")) // replace the type name of the validating TS interface with a temp char start
                    .trimStart())
                    .split("\n");
                //console.log(lines);
                var i = 0;
                for (const line of lines) {
                    if (!line.startsWith(tempCharStart)) {
                        break;
                    }
                    i++;
                }
                return "\n" + lines.slice(0, i)
                    .join("\n")
                    .replace(/, \n/g, "\n\n")
                    .replace(new RegExp(tempCharStart, 'g'), " " + ansicolor.magenta("subject: "))
                    .replace(/ should/g, "\n   " + ansicolor.red("error:") + " should")
                    + "\n";
            }
            error.message = validationErrorFix(error.message);
            error.stack = undefined;
            throw error;
        }
        if (configFileJSON === undefined) {
            const configFileUndefinedError = new Error(`After parsing, the config file ${ansicolor.green(configFileRelativePath)} is undefined`);
            configFileUndefinedError.name = "Config file undefined";
            throw configFileUndefinedError;
        }
        ;
        const packageRootAbsolutePath = path.resolve(".");
        yield validate_presence_in_file_system_1.validateDirectoryPresence(packageRootAbsolutePath, true, packageRoot);
        const packageList = Object.keys(configFileJSON.packages);
        if (packageList.length === 0) {
            log_1.log.error("Config file must have at least one package");
            throw CONFIGURATION_ERROR;
        }
        // Validate each package name
        var foundIssueWithAtLeastOnePackageName = false;
        for (const packageName of packageList) {
            const validationResult = validateNpmPackageName(packageName);
            if (!validationResult.validForNewPackages) {
                log_1.log.error(`'${ansicolor.magenta(packageName)}' is not a valid npm package name`);
                foundIssueWithAtLeastOnePackageName = true;
            }
        }
        if (foundIssueWithAtLeastOnePackageName)
            throw CONFIGURATION_ERROR;
        packageList.forEach(packageName => {
            package_dependency_tracker_1.PackageDependencyTracker.registerPackage(packageName, configFileJSON);
        });
        const leafPackages = package_dependency_tracker_1.PackageDependencyTracker.getLeafSet();
        log_1.log.info(`${leafPackages.size} total leaf packages:\n${Array.from(leafPackages)
            .map(packageName => "          " + ansicolor.magenta(packageName))
            .join("\n")}`);
        const lernaJSONPackagePaths = new Set();
        var anyErrorsPreventingBuild = false;
        // Sync each package name
        for (const packageName of packageList) {
            const nameParts = packageName.split("/");
            const nameIsScoped = nameParts.length === 2;
            const relativePackagePath = packageName; // <---- EDIT HERE
            const packageDirectoryAbsolutePath = path.resolve("./" + relativePackagePath);
            if (nameIsScoped) { // Means we have @scope-name/package-name pattern
                const scopedFolder =  nameParts[0];
                const scopedFolderAbsolutePath = path.resolve("./" + scopedFolder);
                yield validate_presence_in_file_system_1.validateDirectoryPresence(scopedFolderAbsolutePath, true, scopedFolder);
            }
            yield validate_presence_in_file_system_1.validateDirectoryPresence(packageDirectoryAbsolutePath, true, relativePackagePath);
            const packageJSONSyncResult = yield sync_package_json_1.syncPackageJSON(packageName, relativePackagePath, packageDirectoryAbsolutePath, configFileJSON);
            const tsConfigSyncResult = yield sync_tsconfig_json_1.syncTSConfigJSON(packageName, relativePackagePath, nameIsScoped, packageDirectoryAbsolutePath, configFileJSON);
            if (configFileJSON.cleanBeforeCompile) {
                // Removing the tsconfig.tsbuildinfo file in each package will force typescript to recompile everything.
                const buildInfoFilePath = path.join(packageDirectoryAbsolutePath, "tsconfig.tsbuildinfo");
                const buildInfoFileExists = yield fs_async_1.fsAsync.exists(buildInfoFilePath);
                if (buildInfoFileExists) {
                    yield fs_async_1.fsAsync.deleteFile(buildInfoFilePath);
                }
                // Remove the dist folder
                if (tsConfigSyncResult.obj.compilerOptions && tsConfigSyncResult.obj.compilerOptions.outDir !== undefined) {
                    const distDir = tsConfigSyncResult.obj.compilerOptions.outDir;
                    const distDirRelative = path.join(relativePackagePath, distDir)
                        .replace(/\\/g, "/"); // deal with windows
                    const distDirAbsolute = path.resolve("./" + distDirRelative);
                    yield recursive_delete_directory_1.comprehensiveDelete(distDirAbsolute);
                }
            }
            if (configFileJSON.packages[packageName].publishDistributionFolder === true) {
                if (tsConfigSyncResult.obj.compilerOptions && tsConfigSyncResult.obj.compilerOptions.outDir !== undefined) {
                    const outDir = tsConfigSyncResult.obj.compilerOptions.outDir;
                    const packagePublishingRelativeDirectory = path.join(relativePackagePath, outDir)
                        .replace(/\\/g, "/"); // deal with windows
                    const packagePublishingAbsoluteDirectory = path.resolve("./" + packagePublishingRelativeDirectory);
                    log_1.log.info(`${ansicolor.white("publishDistributionFolder")} enabled for package ${ansicolor.magenta(packageName)}.`);
                    // Sync package.json to dist folder
                    yield validate_presence_in_file_system_1.validateDirectoryPresence(packagePublishingAbsoluteDirectory, true, packagePublishingRelativeDirectory);
                    yield fs_async_1.fsAsync.writeFile(path.resolve(packagePublishingAbsoluteDirectory, "package.json"), packageJSONSyncResult.text);
                    lernaJSONPackagePaths.add(packagePublishingRelativeDirectory);
                    const nodeModules = "node_modules";
                    const distributionNodeModulesAbsolutePath = path.resolve(packagePublishingAbsoluteDirectory, nodeModules);
                    yield validate_presence_in_file_system_1.validateDirectoryPresence(distributionNodeModulesAbsolutePath, true, relativePackagePath + "/" + nodeModules);
                    const packageNodeModulesAbsolutePath = path.resolve(packageDirectoryAbsolutePath, nodeModules);
                    yield validate_presence_in_file_system_1.validateSymlinkPresence(packageNodeModulesAbsolutePath, distributionNodeModulesAbsolutePath, false, relativePackagePath + "/" + nodeModules, packagePublishingRelativeDirectory + "/" + nodeModules);
                }
                else {
                    log_1.log.error(`Package ${ansicolor.magenta(packageName)} has ${ansicolor.lightGray(`"${ansicolor.white("publishDistributionFolder")}"`)} enabled, but ts-monorepo requires${"\n          "}that its ${ansicolor.green("tsconfig.json")} contain the field ${ansicolor.lightGray(`"${ansicolor.white("compilerOptions")}"."${ansicolor.white("outDir")}"`)} to enable this feature.`);
                    anyErrorsPreventingBuild = true;
                }
            }
            else {
                lernaJSONPackagePaths.add(relativePackagePath);
            }
        }
        if (anyErrorsPreventingBuild) {
            const preBuildError = new Error("See above messages for details.");
            preBuildError.name = "pre-build error";
            throw preBuildError;
        }
        // Lerna
        yield sync_lerna_json_1.syncLernaJSON(lernaJSONPackagePaths, configFileJSON);
        if (configFileJSON.cleanBeforeCompile) {
            // This will remove all the node_modules folders of each package.
            const lernaCleanCommand = "npx lerna clean --yes";
            const lernaClean = new command_runner_1.CommandRunner(lernaCleanCommand);
            lernaClean.start();
            yield lernaClean.waitUntilDone();
        }
        const lernaInstallAndLinkCommand = "npx lerna bootstrap"; // TODO: add hoisting?
        const lernaBoostrap = new command_runner_1.CommandRunner(lernaInstallAndLinkCommand);
        lernaBoostrap.start();
        yield lernaBoostrap.waitUntilDone();
        // tsc (or ttsc)
        yield sync_tsconfig_leaves_json_1.syncTSConfigLeavesJSON(leafPackages, configFileJSON);
        return configFileJSON.ttypescript;
    });
}
exports.syncPackages = syncPackages;
//# sourceMappingURL=sync-packages.js.map
ozyman42 commented 4 years ago

Sorry for the messiness, this is a pretty early version of the tool. I'm working on the next version right now which mandates yarn v2 actually, and assumes that all monorepo packages are stored in packages, so there's no longer even a packageRoot option.

Are you trying to use something other than the packages folder?

ozyman42 commented 4 years ago

I see. You want packageRoot to be the root of the repo. My concern there is that it doesn't really follow monorepo convention.

hcharley commented 4 years ago

I have used the packages convention before, but given that I have multiple scopes, it's easier for me to have them all to be at the root.

My ideal repo structure is:

@myorg-core/package-1
@myorg-core/utils
@myorg-ui/package-1
@myorg-ui/package-2
@myorg-server/package-1
@myorg-server/package-2
.gitignore
README.md
package.json
yarn.lock
...

Is this something you'd consider support with ts-monorepo? For what it's worth, I've seen this convention used by others.

ozyman42 commented 4 years ago

Seeing as you're the first person to open an issue here, yes I'll add back in the packageRoot option and make sure root of repo is supported.

Originally I was worried about the potential for name conflicts (like someone naming a package .yarn or node_modules), but with the latest I've been working on, the top level folder either needs to be either @scope/package or global-scope/package, so you those naming conflicts should no longer occur.

hcharley commented 4 years ago

@AlexLeung Thank you! If you need a hand, I can take an attempt at filing a PR. I might need direction though, so maybe it will be easier for you to do yourself.

hcharley commented 4 years ago

Just another hack as a note:

Set packageRoot to .

Then change sync-packages.js to:

        // Validate the package root.
        const packageRoot = configFileJSON.packageRoot;
        if (packageRoot.length === 0) {
            log_1.log.error("The 'packageRoot' field may not be empty.");
            throw CONFIGURATION_ERROR;
        }
        if (packageRoot.includes("/") || packageRoot.includes("\\")) {
            log_1.log.error(`The value of the 'packageRoot' field '${ansicolor.white(packageRoot)}' currently contains at least one of the forbidden characters '/' or '\\'.`);
            throw CONFIGURATION_ERROR;
        }
        const packageRootAbsolutePath = packageRoot === "." ? path.resolve(".") : path.resolve(".", packageRoot);
        yield validate_presence_in_file_system_1.validateDirectoryPresence(packageRootAbsolutePath, true, packageRoot);
ozyman42 commented 4 years ago

Probably what I'll do is actually allow packageRoot to be root of repo or any path relative to root as long as it resolves to some place within the repo root folder. Not sure why I limited the entry to only be 1 folder deep in the past.

ozyman42 commented 4 years ago

As for PRs, nobody should be submitting any PRs until the latest version is out. I actually rewrote basically the whole codebase compared to the current master branch.

hcharley commented 4 years ago

Ah, thank you. That makes sense.

hcharley commented 4 years ago

Oh just another note for my usecase... I forgot I have an edgecase, which is beneficial to teammates.

We code a lot of HTML templates, and the developers keep their them in a toplevel folder called html, which is actually a module named @myorg/html.

@myorg-core/package-1
@myorg-core/utils
@myorg-ui/package-1
@myorg-ui/package-2
@myorg-server/package-1
@myorg-server/package-2
html <-----------------------------------------------------
tests
etc
docs
.gitignore
README.md
package.json
yarn.lock
...

So this is not convention, but it is helpful for our HTML engineers. It is not a hard requirement for us though.

ozyman42 commented 4 years ago

maybe you could create a symlink from the toplevel html folder which points to the ts-monorepo managed @myorg/html folder. Not sure if vscode would like this or complain when trying to resolve project references.

ozyman42 commented 3 years ago

With the latest release you can now have a folder structure like this

packages
|_@myorg-core/
|   |_package-1/
|   |_utils/
|_@myorg-ui/
|   |_package-1/
|   |_package-2/
|_@myorg-server/
|   |_package-1/
|   |_package-2/
|_@myorg/
    |_html/

still doesn't allow the root to be the monorepo root, but perhaps this helps?