Browse Source

Revert "chore(core): Removed unused CLI features of core"

This reverts commit 4136f24a
Michael Bromley 1 year ago
parent
commit
2319636179

+ 11 - 0
packages/core/build/tsconfig.cli.json

@@ -0,0 +1,11 @@
+{
+  "extends": "../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../cli"
+  },
+  "files": [
+    "../src/cli/vendure-cli.ts",
+    "../src/cli/index.ts",
+    "../typings.d.ts"
+  ]
+}

+ 1 - 1
packages/core/package.json

@@ -21,7 +21,7 @@
     "scripts": {
         "tsc:watch": "tsc -p ./build/tsconfig.build.json --watch",
         "gulp:watch": "gulp -f ./build/gulpfile.ts watch",
-        "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && gulp -f ./build/gulpfile.ts build",
+        "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && tsc -p ./build/tsconfig.cli.json && gulp -f ./build/gulpfile.ts build",
         "watch": "concurrently yarn:tsc:watch yarn:gulp:watch",
         "lint": "eslint --fix .",
         "test": "vitest --config vitest.config.mts --run",

+ 7 - 0
packages/core/src/cli/cli-utils.ts

@@ -0,0 +1,7 @@
+/* eslint-disable no-console */
+/**
+ * Logs to the console in a fetching blueish color.
+ */
+export function logColored(message: string) {
+    console.log('\x1b[36m%s\x1b[0m', message);
+}

+ 1 - 0
packages/core/src/cli/index.ts

@@ -0,0 +1 @@
+export * from './populate';

+ 154 - 0
packages/core/src/cli/populate.ts

@@ -0,0 +1,154 @@
+import { INestApplicationContext } from '@nestjs/common';
+import fs from 'fs-extra';
+import path from 'path';
+import { lastValueFrom } from 'rxjs';
+
+const loggerCtx = 'Populate';
+
+/* eslint-disable no-console */
+/**
+ * @description
+ * Populates the Vendure server with some initial data and (optionally) product data from
+ * a supplied CSV file. The format of the CSV file is described in the section
+ * [Importing Product Data](/guides/developer-guide/importing-data/).
+ *
+ * If the `channelOrToken` argument is provided, all ChannelAware entities (Products, ProductVariants,
+ * Assets, ShippingMethods, PaymentMethods etc.) will be assigned to the specified Channel.
+ * The argument can be either a Channel object or a valid channel `token`.
+ *
+ * Internally the `populate()` function does the following:
+ *
+ * 1. Uses the {@link Populator} to populate the {@link InitialData}.
+ * 2. If `productsCsvPath` is provided, uses {@link Importer} to populate Product data.
+ * 3. Uses {@link Populator} to populate collections specified in the {@link InitialData}.
+ *
+ * @example
+ * ```ts
+ * import { bootstrap } from '\@vendure/core';
+ * import { populate } from '\@vendure/core/cli';
+ * import { config } from './vendure-config.ts'
+ * import { initialData } from './my-initial-data.ts';
+ *
+ * const productsCsvFile = path.join(__dirname, 'path/to/products.csv')
+ *
+ * populate(
+ *   () => bootstrap(config),
+ *   initialData,
+ *   productsCsvFile,
+ * )
+ * .then(app => app.close())
+ * .then(
+ *   () => process.exit(0),
+ *   err => {
+ *     console.log(err);
+ *     process.exit(1);
+ *   },
+ * );
+ * ```
+ *
+ * @docsCategory import-export
+ */
+export async function populate<T extends INestApplicationContext>(
+    bootstrapFn: () => Promise<T | undefined>,
+    initialDataPathOrObject: string | object,
+    productsCsvPath?: string,
+    channelOrToken?: string | import('@vendure/core').Channel,
+): Promise<T> {
+    const app = await bootstrapFn();
+    if (!app) {
+        throw new Error('Could not bootstrap the Vendure app');
+    }
+    let channel: import('@vendure/core').Channel | undefined;
+    const { ChannelService, Channel, Logger } = await import('@vendure/core');
+    if (typeof channelOrToken === 'string') {
+        channel = await app.get(ChannelService).getChannelFromToken(channelOrToken);
+        if (!channel) {
+            Logger.warn(
+                `Warning: channel with token "${channelOrToken}" was not found. Using default Channel instead.`,
+                loggerCtx,
+            );
+        }
+    } else if (channelOrToken instanceof Channel) {
+        channel = channelOrToken;
+    }
+    const initialData: import('@vendure/core').InitialData =
+        typeof initialDataPathOrObject === 'string'
+            ? require(initialDataPathOrObject)
+            : initialDataPathOrObject;
+
+    await populateInitialData(app, initialData, channel);
+
+    if (productsCsvPath) {
+        const importResult = await importProductsFromCsv(
+            app,
+            productsCsvPath,
+            initialData.defaultLanguage,
+            channel,
+        );
+        if (importResult.errors && importResult.errors.length) {
+            const errorFile = path.join(process.cwd(), 'vendure-import-error.log');
+            Logger.error(
+                `${importResult.errors.length} errors encountered when importing product data. See: ${errorFile}`,
+                loggerCtx,
+            );
+            await fs.writeFile(errorFile, importResult.errors.join('\n'));
+        }
+
+        Logger.info(`Imported ${importResult.imported} products`, loggerCtx);
+
+        await populateCollections(app, initialData, channel);
+    }
+
+    Logger.info('Done!', loggerCtx);
+    return app;
+}
+
+export async function populateInitialData(
+    app: INestApplicationContext,
+    initialData: import('@vendure/core').InitialData,
+    channel?: import('@vendure/core').Channel,
+) {
+    const { Populator, Logger } = await import('@vendure/core');
+    const populator = app.get(Populator);
+    try {
+        await populator.populateInitialData(initialData, channel);
+        Logger.info('Populated initial data', loggerCtx);
+    } catch (err: any) {
+        Logger.error(err.message, loggerCtx);
+    }
+}
+
+export async function populateCollections(
+    app: INestApplicationContext,
+    initialData: import('@vendure/core').InitialData,
+    channel?: import('@vendure/core').Channel,
+) {
+    const { Populator, Logger } = await import('@vendure/core');
+    const populator = app.get(Populator);
+    try {
+        if (initialData.collections.length) {
+            await populator.populateCollections(initialData, channel);
+            Logger.info(`Created ${initialData.collections.length} Collections`, loggerCtx);
+        }
+    } catch (err: any) {
+        Logger.info(err.message, loggerCtx);
+    }
+}
+
+export async function importProductsFromCsv(
+    app: INestApplicationContext,
+    productsCsvPath: string,
+    languageCode: import('@vendure/core').LanguageCode,
+    channel?: import('@vendure/core').Channel,
+): Promise<import('@vendure/core').ImportProgress> {
+    const { Importer, RequestContextService } = await import('@vendure/core');
+    const importer = app.get(Importer);
+    const requestContextService = app.get(RequestContextService);
+    const productData = await fs.readFile(productsCsvPath, 'utf-8');
+    const ctx = await requestContextService.create({
+        apiType: 'admin',
+        languageCode,
+        channelOrToken: channel,
+    });
+    return lastValueFrom(importer.parseAndImport(productData, ctx, true));
+}

+ 133 - 0
packages/core/src/cli/vendure-cli.ts

@@ -0,0 +1,133 @@
+#!/usr/bin/env node
+/* eslint-disable @typescript-eslint/no-var-requires */
+import { INestApplication } from '@nestjs/common';
+import program from 'commander';
+import fs from 'fs-extra';
+import path from 'path';
+
+import { logColored } from './cli-utils';
+import { importProductsFromCsv, populateCollections, populateInitialData } from './populate';
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const version = require('../../package.json').version;
+
+/* eslint-disable no-console */
+logColored(`
+                      _
+                     | |
+ __   _____ _ __   __| |_   _ _ __ ___
+ \\ \\ / / _ \\ '_ \\ / _\` | | | | '__/ _ \\
+  \\ V /  __/ | | | (_| | |_| | | |  __/
+   \\_/ \\___|_| |_|\\__,_|\\__,_|_|  \\___|
+                                       `);
+
+program.version(`Vendure CLI v${version as string}`, '-v --version').name('vendure');
+
+program
+    .command('import-products <csvFile>')
+    .option('-l, --language', 'Specify ISO 639-1 language code, e.g. "de", "es". Defaults to "en"')
+    .description('Import product data from the specified csv file')
+    .action(async (csvPath, command) => {
+        const filePath = path.join(process.cwd(), csvPath);
+        await importProducts(filePath, command.language);
+    });
+program
+    .command('init <initDataFile>')
+    .description('Import initial data from the specified json file')
+    .action(async (initDataFile, command) => {
+        const filePath = path.join(process.cwd(), initDataFile);
+        logColored(`\nPopulating initial data from "${filePath}"...\n`);
+        const initialData = require(filePath);
+        const app = await getApplicationRef();
+        if (app) {
+            await populateInitialData(app, initialData);
+            logColored('\nDone!');
+            await app.close();
+        }
+        process.exit(0);
+    });
+program
+    .command('create-collections <initDataFile>')
+    .description('Create collections from the specified json file')
+    .action(async (initDataFile, command) => {
+        const filePath = path.join(process.cwd(), initDataFile);
+        logColored(`\nCreating collections from "${filePath}"...\n`);
+        const initialData = require(filePath);
+        const app = await getApplicationRef();
+        if (app) {
+            await populateCollections(app, initialData);
+            logColored('\nDone!');
+            await app.close();
+        }
+        process.exit(0);
+    });
+program.parse(process.argv);
+if (!process.argv.slice(2).length) {
+    program.help();
+}
+
+async function importProducts(csvPath: string, languageCode: import('@vendure/core').LanguageCode) {
+    logColored(`\nImporting from "${csvPath}"...\n`);
+    const app = await getApplicationRef();
+    if (app) {
+        await importProductsFromCsv(app, csvPath, languageCode);
+        logColored('\nDone!');
+        await app.close();
+        process.exit(0);
+    }
+}
+
+async function getApplicationRef(): Promise<INestApplication | undefined> {
+    const tsConfigFile = path.join(process.cwd(), 'vendure-config.ts');
+    const jsConfigFile = path.join(process.cwd(), 'vendure-config.js');
+    let isTs = false;
+    let configFile: string | undefined;
+    if (fs.existsSync(tsConfigFile)) {
+        configFile = tsConfigFile;
+        isTs = true;
+    } else if (fs.existsSync(jsConfigFile)) {
+        configFile = jsConfigFile;
+    }
+
+    if (!configFile) {
+        console.error('Could not find a config file');
+        console.error(`Checked "${tsConfigFile}", "${jsConfigFile}"`);
+        process.exit(1);
+        return;
+    }
+
+    if (isTs) {
+        // we expect ts-node to be available
+        const tsNode = require('ts-node');
+        if (!tsNode) {
+            console.error('For "populate" to work with TypeScript projects, you must have ts-node installed');
+            process.exit(1);
+            return;
+        }
+        require('ts-node').register();
+    }
+
+    const index = require(configFile);
+
+    if (!index) {
+        console.error(`Could not read the contents of "${configFile}"`);
+        process.exit(1);
+        return;
+    }
+    if (!index.config) {
+        console.error(`The file "${configFile}" does not export a "config" object`);
+        process.exit(1);
+        return;
+    }
+
+    const config = index.config;
+
+    // Force the sync mode on, so that all the tables are created
+    // on this initial run.
+    config.dbConnectionOptions.synchronize = true;
+
+    const { bootstrap } = require('@vendure/core');
+    console.log('Bootstrapping Vendure server...');
+    const app = await bootstrap(config);
+    return app;
+}