## Introduction
EverShop is a modern, TypeScript-first eCommerce platform built with GraphQL and React. Designed for developers, it offers essential commerce features in a modular, fully customizable architecture—perfect for building tailored shopping experiences with confidence and speed.
## Installation Using Docker
You can get started with EverShop in minutes by using the Docker image. The Docker image is a great way to get started with EverShop without having to worry about installing dependencies or configuring your environment.
```bash
curl -sSL https://raw.githubusercontent.com/evershopcommerce/evershop/main/docker-compose.yml > docker-compose.yml
docker compose up -d
```
For the full installation guide, please refer to our [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide).
## Documentation
- [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide).
- [Extension development](https://evershop.io/docs/development/module/create-your-first-extension).
- [Theme development](https://evershop.io/docs/development/theme/theme-overview).
## Demo
Explore our demo store.
Demo user:
Email: demo@evershop.io
Password: 123456
## Support
If you like my work, feel free to:
- ⭐ this repository. It helps.
- [][tweet] about EverShop. Thank you!
[tweet]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fevershopcommerce%2Fevershop&text=Awesome%20React%20Ecommerce%20Project&hashtags=react,ecommerce,expressjs,graphql
## Contributing
EverShop is an open-source project. We are committed to a fully transparent development process and appreciate highly any contributions. Whether you are helping us fix bugs, proposing new features, improving our documentation or spreading the word - we would love to have you as part of the EverShop community.
### Ask a question about EverShop
You can ask questions, and participate in discussions about EverShop-related topics in the EverShop Discord channel.
### Create a bug report
If you see an error message or run into an issue, please [create bug report](https://github.com/evershopcommerce/evershop/issues/new). This effort is valued and it will help all EverShop users.
### Submit a feature request
If you have an idea, or you're missing a capability that would make development easier and more robust, please [Submit feature request](https://github.com/evershopcommerce/evershop/issues/new).
If a similar feature request already exists, don't forget to leave a "+1".
If you add some more information such as your thoughts and vision about the feature, your comments will be embraced warmly :)
Please refer to our [Contribution Guidelines](./CONTRIBUTING.md) and [Code of Conduct](./CODE_OF_CONDUCT.md).
## 🚀 The Future of EverShop
EverShop is seeing rapid organic growth and strong adoption from the developer community. We are now scaling our operations and building **EverShop Cloud**.
If you are a strategic investor interested in the future of Node.js commerce and our mission to set a new standard for modern eCommerce, we’d love to share our vision and roadmap with you.
📩 **Get in touch:** support@evershop.io
## License
[GPL-3.0 License](https://github.com/evershopcommerce/evershop/blob/main/LICENSE)
================================================
FILE: docker-compose.yml
================================================
version: '3.8'
services:
app:
image: evershop/evershop:latest
restart: always
environment:
DB_HOST: database
DB_PORT: 5432
DB_PASSWORD: postgres
DB_USER: postgres
DB_NAME: postgres
networks:
- myevershop
depends_on:
- database
ports:
- 3000:3000
#The postgres database:
database:
image: postgres:16
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
networks:
- myevershop
networks:
myevershop:
name: MyEverShop
driver: bridge
volumes:
postgres-data:
================================================
FILE: eslint.config.js
================================================
// eslint.config.js
import eslintPluginTypescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import pluginImport from 'eslint-plugin-import';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import pluginReact from 'eslint-plugin-react';
export default [
{
ignores: [
"/node_modules/",
"**/*test.js",
"**/tests/**",
"**/create-evershop-app/**",
"**/.evershop/**",
"/.vscode/**",
"/.git/**",
"/.idea/**",
"**/extensions/**",
"**/public/**",
"**/themes/**",
"**/media/**",
"**/dist/**",
"**/packages/*/dist/**",
"**/packages/evershop/dist/**",
"**/packages/postgres-query-builder/dist/**",
"**/packages/product_review/**",
"**/packages/resend/**"
]},
pluginReact.configs.flat.recommended,
pluginReact.configs.flat['jsx-runtime'],
jsxA11y.flatConfigs.recommended,
{
files: ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
plugins: {
"@typescript-eslint": eslintPluginTypescript
},
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
}
},
{
plugins: {
"import": pluginImport
},
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions : {
ecmaFeatures: {
jsx: true
}
}
},
rules: {
semi: "off",
"prefer-const": "error",
"import/no-dynamic-require": 0,
"no-else-return": "off",
"import/prefer-default-export": 0,
"jsx-a11y/anchor-is-valid": 0,
"import/no-extraneous-dependencies": "off",
"import/no-unresolved": "off",
"camelcase": "off",
"no-multi-assign": "off",
"no-template-curly-in-string": "off",
"react/no-array-index-key": "off",
"react/no-unstable-nested-components": "off",
"no-continue": "off",
"no-await-in-loop": "off",
"no-use-before-define" : "off",
"global-require": "off",
"import/extensions": "off",
"no-shadow": "off",
"no-lonely-if": "warn",
"no-console": "error",
"no-useless-return": "off",
"react/display-name": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/role-supports-aria-props": "warn",
"jsx-a11y/no-noninteractive-element-interactions": "warn",
// Add import sorting rules
"import/order": ["warn", {
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"newlines-between": "never",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}]
},
settings: {
react: {
version: "detect"
}
}
}
]
================================================
FILE: jest.config.js
================================================
export default {
testEnvironment: "node",
moduleNameMapper: {
'^@evershop/postgres-query-builder$': '/packages/postgres-query-builder/dist/index.js',
'^@evershop/postgres-query-builder/(.*)$': '/packages/postgres-query-builder/dist/$1',
'^(\\.{1,2}/.*)\\.js$': '$1'
},
transformIgnorePatterns: [
"/node_modules/(?!(@evershop)/)"
],
testMatch: ["**/dist/**/tests/**/unit/**/*.test.[jt]s"],
modulePathIgnorePatterns: ["/packages/evershop/src/"]
};
================================================
FILE: package.json
================================================
{
"name": "evershop",
"version": "2.1.0",
"type": "module",
"description": "A shopping cart platform with Express, React and Postgres",
"workspaces": [
"packages/*",
"extensions/*"
],
"scripts": {
"dev": "node ./packages/evershop/dist/bin/dev/index.js",
"start": "node ./packages/evershop/dist/bin/start/index.js",
"build": "node ./packages/evershop/dist/bin/build/index.js",
"build-fast": "evershop build -- --skip-minify",
"setup": "evershop install",
"theme:active": "evershop theme:active",
"theme:twizz": "evershop theme:twizz",
"theme:create": "evershop theme:create",
"compile": "rimraf ./packages/evershop/dist && cd ./packages/evershop && swc ./src/ -d dist/ --config-file .swcrc --copy-files --strip-leading-paths",
"compile:db": "rimraf ./packages/postgres-query-builder/dist && cd ./packages/postgres-query-builder && swc ./src/ -d dist/ --config-file .swcrc --copy-files --strip-leading-paths",
"compile:tsc": "rimraf ./packages/evershop/dist && cd ./packages/evershop && tsc && copyfiles -u 1 \"src/**/*.{graphql,scss,css,json}\" dist",
"start:debug": "node ./packages/evershop/dist/bin/start/index.js --debug",
"test": "ALLOW_CONFIG_MUTATIONS=true NODE_OPTIONS=--experimental-vm-modules node_modules/jest/bin/jest.js",
"lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./packages",
"prepare": "husky install"
},
"author": "The Nguyen (https://evershop.io)",
"license": "GNU GENERAL PUBLIC LICENSE 3.0",
"devDependencies": {
"@parcel/watcher": "^2.5.1",
"@swc/cli": "^0.7.7",
"@swc/core": "^1.11.29",
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.10",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"copyfiles": "^2.4.1",
"cypress": "^13.15.1",
"eslint": "^9.24.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"execa": "^9.6.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"prettier": "2.8.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^6.0.1",
"swc-minify-webpack-plugin": "^2.1.3",
"tailwindcss": "^4.1.18",
"typescript": "^5.8.3",
"webpack-bundle-analyzer": "^4.10.2"
},
"dependencies": {
"@sendgrid/mail": "^8.1.6",
"@types/react-slick": "^0.23.13",
"@types/uuid": "^10.0.0",
"uuid": "^13.0.0"
}
}
================================================
FILE: packages/create-evershop-app/README.md
================================================
# create-evershop-app
This package includes the global command for [Create EverShop App](https://evershop.io/). Please refer to its documentation:
- [Getting Started](https://evershop.io/docs/development/getting-started/introduction) – How to create a new app.
- [Development Guide](https://evershop.io/docs/development/) – How to develop an ecommerce web app with EverShop.
================================================
FILE: packages/create-evershop-app/createEverShopApp.js
================================================
const https = require('https');
const chalk = require('chalk');
const commander = require('commander');
const dns = require('dns');
const { execSync } = require('child_process');
const fs = require('fs-extra');
const os = require('os');
const path = require('path');
const semver = require('semver');
const spawn = require('cross-spawn');
const url = require('url');
const validateProjectName = require('validate-npm-package-name');
const { mkdir } = require('fs/promises');
const packageJson = require('./package.json');
function isUsingYarn() {
return (process.env.npm_config_user_agent || '').indexOf('yarn') === 0;
}
let projectName;
function init() {
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments('[project-directory]')
.usage(`${chalk.green('')} [options]`)
.action((name) => {
projectName = name;
})
.option('--verbose', 'Print additional logs')
.option('--info', 'Print environment debug info')
.on('--help', () => {
console.log(
` Only ${chalk.green('')} is required.`
);
console.log();
console.log(
` If you have any problems, do not hesitate to file an issue:`
);
console.log(
` ${chalk.cyan(
'https://github.com/evershop/create-evershop-app/issues/new'
)}`
);
console.log();
})
.parse(process.argv);
const options = program.opts();
if (typeof projectName === 'undefined') {
console.error('Please specify the project directory:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('')}`
);
console.log();
console.log('For example:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('my-evershop-app')}`
);
console.log();
console.log(
`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
);
process.exit(1);
}
// We first check the registry directly via the API, and if that fails, we try
// the slower `npm view [package] version` command.
//
// This is important for users in environments where direct access to npm is
// blocked by a firewall, and packages are provided exclusively via a private
// registry.
checkForLatestVersion()
.catch(() => {
try {
return execSync('npm view create-evershop-app version')
.toString()
.trim();
} catch (e) {
return null;
}
})
.then((latest) => {
if (latest && semver.lt(packageJson.version, latest)) {
console.log();
console.error(
chalk.yellow(
`You are running \`create-evershop-app\` ${packageJson.version}, which is behind the latest release (${latest}).\n\n` +
'We recommend always using the latest version of create-evershop-app if possible.'
)
);
console.log();
console.log(
'The latest instructions for creating a new app can be found here:\n' +
'https://evershop.io/docs/development/getting-started/installation-guide/'
);
console.log();
} else {
const useYarn = isUsingYarn();
createApp(projectName, options.verbose, useYarn);
}
});
}
function createApp(name, verbose, useYarn) {
const root = path.resolve(name);
const appName = path.basename(root);
checkAppName(appName);
fs.ensureDirSync(name);
if (!isSafeToCreateProjectIn(root, name)) {
process.exit(1);
}
console.log();
console.log(`Creating a new EverShop app in ${chalk.green(root)}.`);
console.log();
const packageJson = {
name: appName,
version: '0.1.0',
type: 'module',
private: true,
scripts: {
setup: 'evershop install',
start: 'evershop start',
build: 'evershop build',
dev: 'evershop dev'
}
};
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
const originalDirectory = process.cwd();
process.chdir(root);
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
if (!useYarn) {
const npmInfo = checkNpmVersion();
if (!npmInfo.hasMinNpm) {
if (npmInfo.npmVersion) {
console.log(
chalk.yellow(
`Please update to npm 7 or higher for a workspaces feature.\n`
)
);
}
}
}
run(root, appName, verbose, originalDirectory, useYarn);
}
function install(root, useYarn, dependencies, verbose, isOnline) {
return new Promise((resolve, reject) => {
let command;
let args;
if (useYarn) {
command = 'yarnpkg';
args = ['add', '--exact'];
if (!isOnline) {
args.push('--offline');
}
[].push.apply(args, dependencies);
// Explicitly set cwd() to work around issues like
// https://github.com/facebook/create-react-app/issues/3326.
// Unfortunately we can only do this for Yarn because npm support for
// equivalent --prefix flag doesn't help with this issue.
// This is why for npm, we run checkThatNpmCanReadCwd() early instead.
args.push('--cwd');
args.push(root);
if (!isOnline) {
console.log(chalk.yellow('You appear to be offline.'));
console.log(chalk.yellow('Falling back to the local Yarn cache.'));
console.log();
}
} else {
command = 'npm';
args = [
'install',
'--no-audit', // https://github.com/facebook/create-evershop-app/issues/11174
'--save',
'--save-exact',
'--loglevel',
'error'
].concat(dependencies);
}
if (verbose) {
args.push('--verbose');
}
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', (code) => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`
});
return;
}
resolve();
});
});
}
function installDevDependencies(
root,
useYarn,
devDependencies,
verbose,
isOnline
) {
console.log(`Installing some development dependencies...`);
return new Promise((resolve, reject) => {
let command;
let args;
if (useYarn) {
command = 'yarnpkg';
args = ['add', '--exact'];
if (!isOnline) {
args.push('--offline');
}
[].push.apply(args, devDependencies);
args.push('--dev');
// Explicitly set cwd() to work around issues like
// https://github.com/facebook/create-react-app/issues/3326.
// Unfortunately we can only do this for Yarn because npm support for
// equivalent --prefix flag doesn't help with this issue.
// This is why for npm, we run checkThatNpmCanReadCwd() early instead.
args.push('--cwd');
args.push(root);
if (!isOnline) {
console.log(chalk.yellow('You appear to be offline.'));
console.log(chalk.yellow('Falling back to the local Yarn cache.'));
console.log();
}
} else {
command = 'npm';
args = [
'install',
'--no-audit', // https://github.com/facebook/create-evershop-app/issues/11174
'--save',
'--save-exact',
'--loglevel',
'error'
].concat(devDependencies);
args.push('--save-dev');
}
if (verbose) {
args.push('--verbose');
}
const child = spawn(command, args, { stdio: 'inherit' });
child.on('close', (code) => {
if (code !== 0) {
reject({
command: `${command} ${args.join(' ')}`
});
return;
}
resolve();
});
});
}
function run(root, appName, verbose, originalDirectory, useYarn) {
console.log(`Installing ${chalk.cyan('@evershop/evershop')}`);
checkIfOnline(useYarn)
.then((isOnline) => ({
isOnline
}))
.then(({ isOnline }) => {
const allDependencies = ['@evershop/evershop'];
return install(root, useYarn, allDependencies, verbose, isOnline).then(
async () => {
await installDevDependencies(
root,
useYarn,
[
'@parcel/watcher',
'@types/config',
'@types/express',
'@types/node',
'@types/pg',
'@types/react',
'execa',
'typescript'
],
verbose,
isOnline
);
await createConfigFile(root);
await createSampleExtension(root);
await createSampleTheme(root);
await setUpEverShop(root);
}
);
})
.catch((reason) => {
console.log(reason);
console.log();
console.log('Aborting installation.');
if (reason.command) {
console.log(` ${chalk.cyan(reason.command)} has failed.`);
} else {
console.log(chalk.red('Unexpected error. Please report it as a bug:'));
console.log(reason);
}
console.log();
// On 'exit' we will delete these files from target directory.
const knownGeneratedFiles = [
'package.json',
'node_modules',
'package-lock.json'
];
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach((file) => {
knownGeneratedFiles.forEach((fileToMatch) => {
// This removes all knownGeneratedFiles.
if (file === fileToMatch) {
console.log(`Deleting generated file... ${chalk.cyan(file)}`);
fs.removeSync(path.join(root, file));
}
});
});
const remainingFiles = fs.readdirSync(path.join(root));
if (!remainingFiles.length) {
// Delete target folder if empty
console.log(
`Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(
path.resolve(root, '..')
)}`
);
process.chdir(path.resolve(root, '..'));
fs.removeSync(path.join(root));
}
console.log('Done.');
process.exit(1);
});
}
function checkNpmVersion() {
let hasMinNpm = true;
let npmVersion = null;
try {
npmVersion = execSync('npm --version').toString().trim();
hasMinNpm = semver.gte(npmVersion, '7.0.0');
} catch (err) {
// ignore
}
return {
hasMinNpm,
npmVersion
};
}
function checkAppName(appName) {
const validationResult = validateProjectName(appName);
if (appName === 'dist') {
console.error(
chalk.red(
`Cannot create a project named ${chalk.green(
`"${appName}"`
)} because it is reserved for the distribution files.\n` +
`Please choose a different project name.`
)
);
process.exit(1);
}
if (!validationResult.validForNewPackages) {
console.error(
chalk.red(
`Cannot create a project named ${chalk.green(
`"${appName}"`
)} because of npm naming restrictions:\n`
)
);
[
...(validationResult.errors || []),
...(validationResult.warnings || [])
].forEach((error) => {
console.error(chalk.red(` * ${error}`));
});
console.error(chalk.red('\nPlease choose a different project name.'));
process.exit(1);
}
// TODO: there should be a single place that holds the dependencies
const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
if (dependencies.includes(appName)) {
console.error(
chalk.red(
`Cannot create a project named ${chalk.green(
`"${appName}"`
)} because a dependency with the same name exists.\n` +
`Due to the way npm works, the following names are not allowed:\n\n`
) +
chalk.cyan(dependencies.map((depName) => ` ${depName}`).join('\n')) +
chalk.red('\n\nPlease choose a different project name.')
);
process.exit(1);
}
}
// If project only contains files generated by GH, it’s safe.
// Also, if project contains remnant error logs from a previous
// installation, lets remove them now.
// We also special case IJ-based products .idea because it integrates with CRA:
// https://github.com/facebook/create-evershop-app/pull/368#issuecomment-243446094
function isSafeToCreateProjectIn(root, name) {
const validFiles = [
'.DS_Store',
'.git',
'.gitattributes',
'.gitignore',
'.gitlab-ci.yml',
'.hg',
'.hgcheck',
'.hgignore',
'.idea',
'.npmignore',
'.travis.yml',
'docs',
'LICENSE',
'README.md',
'mkdocs.yml',
'Thumbs.db'
];
// These files should be allowed to remain on a failed install, but then
// silently removed during the next create.
const errorLogFilePatterns = [
'npm-debug.log',
'yarn-error.log',
'yarn-debug.log'
];
const isErrorLog = (file) =>
errorLogFilePatterns.some((pattern) => file.startsWith(pattern));
const conflicts = fs
.readdirSync(root)
.filter((file) => !validFiles.includes(file))
// IntelliJ IDEA creates module files before CRA is launched
.filter((file) => !/\.iml$/.test(file))
// Don't treat log files from previous installation as conflicts
.filter((file) => !isErrorLog(file));
if (conflicts.length > 0) {
console.log(
`The directory ${chalk.green(name)} contains files that could conflict:`
);
console.log();
for (const file of conflicts) {
try {
const stats = fs.lstatSync(path.join(root, file));
if (stats.isDirectory()) {
console.log(` ${chalk.blue(`${file}/`)}`);
} else {
console.log(` ${file}`);
}
} catch (e) {
console.log(` ${file}`);
}
}
console.log();
console.log(
'Either try using a new directory name, or remove the files listed above.'
);
return false;
}
// Remove any log files from a previous installation.
fs.readdirSync(root).forEach((file) => {
if (isErrorLog(file)) {
fs.removeSync(path.join(root, file));
}
});
return true;
}
function getProxy() {
if (process.env.https_proxy) {
return process.env.https_proxy;
} else {
try {
// Trying to read https-proxy from .npmrc
const httpsProxy = execSync('npm config get https-proxy')
.toString()
.trim();
return httpsProxy !== 'null' ? httpsProxy : undefined;
} catch (e) {}
}
}
// See https://github.com/facebook/create-evershop-app/pull/3355
function checkThatNpmCanReadCwd() {
const cwd = process.cwd();
let childOutput = null;
try {
// Note: intentionally using spawn over exec since
// the problem doesn't reproduce otherwise.
// `npm config list` is the only reliable way I could find
// to reproduce the wrong path. Just printing process.cwd()
// in a Node process was not enough.
childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
} catch (err) {
// Something went wrong spawning node.
// Not great, but it means we can't do this check.
// We might fail later on, but let's continue.
return true;
}
if (typeof childOutput !== 'string') {
return true;
}
const lines = childOutput.split('\n');
// `npm config list` output includes the following line:
// "; cwd = C:\path\to\current\dir" (unquoted)
// I couldn't find an easier way to get it.
const prefix = '; cwd = ';
const line = lines.find((line) => line.startsWith(prefix));
if (typeof line !== 'string') {
// Fail gracefully. They could remove it.
return true;
}
const npmCWD = line.substring(prefix.length);
if (npmCWD === cwd) {
return true;
}
console.error(
chalk.red(
`Could not start an npm process in the right directory.\n\n` +
`The current directory is: ${chalk.bold(cwd)}\n` +
`However, a newly started npm process runs in: ${chalk.bold(
npmCWD
)}\n\n` +
`This is probably caused by a misconfigured system terminal shell.`
)
);
if (process.platform === 'win32') {
console.error(
`${chalk.red(
`On Windows, this can usually be fixed by running:\n\n`
)} ${chalk.cyan(
'reg'
)} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
` ${chalk.cyan(
'reg'
)} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n${chalk.red(
`Try to run the above two lines in the terminal.\n`
)}${chalk.red(
`To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`
)}`
);
}
return false;
}
function checkIfOnline(useYarn) {
if (!useYarn) {
// Don't ping the Yarn registry.
// We'll just assume the best case.
return Promise.resolve(true);
}
return new Promise((resolve) => {
dns.lookup('registry.yarnpkg.com', (err) => {
let proxy;
if (err != null && (proxy = getProxy())) {
// If a proxy is defined, we likely can't resolve external hostnames.
// Try to resolve the proxy name as an indication of a connection.
dns.lookup(url.parse(proxy).hostname, (proxyErr) => {
resolve(proxyErr == null);
});
} else {
resolve(err == null);
}
});
});
}
async function setUpEverShop(projectDir) {
// Use spawn to run 'npm run setup' command from the project directory
await new Promise((resolve, reject) => {
const child = spawn('npm', ['run', 'setup'], {
cwd: projectDir,
stdio: 'inherit'
});
child.on('close', (code) => {
if (code !== 0) {
reject({
command: 'npm run setup'
});
return;
}
resolve();
});
});
}
async function createConfigFile(projectDir) {
console.log(
`Creating ${chalk.cyan('config/default.json')} in ${chalk.green(
projectDir
)}`
);
const config = {
shop: {
language: 'en',
currency: 'USD'
},
system: {
extensions: [
{
name: 'sample',
resolve: 'extensions/sample',
enabled: true
}
],
theme: 'sample'
}
};
await mkdir(path.resolve(projectDir, 'config'), { recursive: true });
fs.writeFileSync(
path.join(projectDir, 'config', 'default.json'),
JSON.stringify(config, null, 2) + os.EOL
);
}
async function createSampleExtension(projectDir) {
console.log(
`Creating ${chalk.cyan('extensions/sample')} in ${chalk.green(projectDir)}`
);
// Copy the extensions folder from the package to the project directory
const sourceDir = path.resolve(__dirname, 'sample/extensions');
const targetDir = path.resolve(projectDir, 'extensions');
await fs.copy(sourceDir, targetDir);
}
async function createSampleTheme(projectDir) {
console.log(
`Creating ${chalk.cyan('themes/sample')} in ${chalk.green(projectDir)}`
);
// Copy the themes folder from the package to the project directory
const sourceDir = path.resolve(__dirname, 'sample/themes');
const targetDir = path.resolve(projectDir, 'themes');
await fs.copy(sourceDir, targetDir);
}
function checkForLatestVersion() {
return new Promise((resolve, reject) => {
https
.get(
'https://registry.npmjs.org/-/package/create-evershop-app/dist-tags',
(res) => {
if (res.statusCode === 200) {
let body = '';
res.on('data', (data) => (body += data));
res.on('end', () => {
resolve(JSON.parse(body).latest);
});
} else {
reject();
}
}
)
.on('error', () => {
reject();
});
});
}
module.exports = {
init
};
================================================
FILE: packages/create-evershop-app/index.js
================================================
#!/usr/bin/env node
const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('.');
const major = semver[0];
if (major < 14) {
console.error(
`You are running Node ${currentNodeVersion}.\n` +
`Create React App requires Node 14 or higher. \n` +
`Please update your version of Node.`
);
process.exit(1);
}
const { init } = require('./createEverShopApp');
init();
================================================
FILE: packages/create-evershop-app/package.json
================================================
{
"name": "create-evershop-app",
"version": "2.3.0",
"description": "Create EverShop App",
"main": "index.js",
"files": [
"index.js",
"createEverShopApp.js",
"sample"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"create-evershop-app": "./index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/evershopcommerce/evershop.git"
},
"author": "The Nguyen",
"license": "ISC",
"bugs": {
"url": "https://github.com/evershopcommerce/evershop/issues"
},
"homepage": "https://github.com/evershopcommerce/evershop#readme",
"dependencies": {
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"commander": "^9.4.1",
"cross-spawn": "^7.0.3",
"fs-extra": "^10.0.0",
"semver": "^7.6.3",
"validate-npm-package-name": "^4.0.0"
}
}
================================================
FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/all/EveryWhere.d.ts
================================================
import React from 'react';
export default function EveryWhere(): React.JSX.Element;
export declare const layout: {
areaId: string;
sortOrder: number;
};
================================================
FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/all/EveryWhere.js
================================================
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.layout = void 0;
exports.default = EveryWhere;
const react_1 = __importDefault(require("react"));
function EveryWhere() {
return (react_1.default.createElement("div", { className: "container mx-auto px-4 py-8 bg-gray-100 rounded-lg shadow-md mt-10" },
react_1.default.createElement("h1", { className: "font-bold text-center mb-6" }, "Everywhere"),
react_1.default.createElement("p", { className: "text-gray-700 text-center" }, "This component is rendered on every page of the store front."),
react_1.default.createElement("p", { className: "text-gray-700 text-center" },
"You can modify this component at",
' ',
react_1.default.createElement("code", null, "`themes/sample/src/pages/all/EveryWhere.tsx`")),
react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "You can also remove this by disabling the theme `sample`.")));
}
exports.layout = {
areaId: 'content',
sortOrder: 20
};
//# sourceMappingURL=EveryWhere.js.map
================================================
FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/all/EveryWhere.js.map
================================================
{"version":3,"file":"EveryWhere.js","sourceRoot":"","sources":["../../../src/pages/all/EveryWhere.tsx"],"names":[],"mappings":";;;;;;AAEA,6BAgBC;AAlBD,kDAA0B;AAE1B,SAAwB,UAAU;IAChC,OAAO,CACL,uCAAK,SAAS,EAAC,oEAAoE;QACjF,sCAAI,SAAS,EAAC,4BAA4B,iBAAgB;QAC1D,qCAAG,SAAS,EAAC,2BAA2B,mEAEpC;QACJ,qCAAG,SAAS,EAAC,2BAA2B;;YACL,GAAG;YACpC,2FAAyD,CACvD;QACJ,qCAAG,SAAS,EAAC,4BAA4B,gEAErC,CACA,CACP,CAAC;AACJ,CAAC;AAEY,QAAA,MAAM,GAAG;IACpB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,EAAE;CACd,CAAC"}
================================================
FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/homepage/OnlyHomePage.d.ts
================================================
import React from 'react';
export default function OnlyHomePage(): React.JSX.Element;
export declare const layout: {
areaId: string;
sortOrder: number;
};
================================================
FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/homepage/OnlyHomePage.js
================================================
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.layout = void 0;
exports.default = OnlyHomePage;
const react_1 = __importDefault(require("react"));
function OnlyHomePage() {
return (react_1.default.createElement("div", { className: "container mx-auto px-4 py-8 bg-gray-100 rounded-lg shadow-md mt-10" },
react_1.default.createElement("h1", { className: "font-bold text-center mb-6" }, "Home Page Only"),
react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "This component is only rendered on the home page."),
react_1.default.createElement("p", { className: " text-gray-700 text-center" },
"You can modify this component at",
' ',
react_1.default.createElement("code", null, "`themes/sample/src/pages/homepage/OnlyHomePage.tsx`")),
react_1.default.createElement("p", { className: " text-gray-700 text-center" }, "You can also remove this by disabling the theme `sample`.")));
}
exports.layout = {
areaId: 'content',
sortOrder: 10
};
//# sourceMappingURL=OnlyHomePage.js.map
================================================
FILE: packages/create-evershop-app/sample/themes/sample/dist/pages/homepage/OnlyHomePage.js.map
================================================
{"version":3,"file":"OnlyHomePage.js","sourceRoot":"","sources":["../../../src/pages/homepage/OnlyHomePage.tsx"],"names":[],"mappings":";;;;;;AAEA,+BAgBC;AAlBD,kDAA0B;AAE1B,SAAwB,YAAY;IAClC,OAAO,CACL,uCAAK,SAAS,EAAC,oEAAoE;QACjF,sCAAI,SAAS,EAAC,4BAA4B,qBAAoB;QAC9D,qCAAG,SAAS,EAAC,4BAA4B,wDAErC;QACJ,qCAAG,SAAS,EAAC,4BAA4B;;YACN,GAAG;YACpC,kGAAgE,CAC9D;QACJ,qCAAG,SAAS,EAAC,4BAA4B,gEAErC,CACA,CACP,CAAC;AACJ,CAAC;AAEY,QAAA,MAAM,GAAG;IACpB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,EAAE;CACd,CAAC"}
================================================
FILE: packages/create-evershop-app/sample/themes/sample/package.json
================================================
{
"name": "sample-evershop-theme",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"tsc": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
================================================
FILE: packages/create-evershop-app/sample/themes/sample/src/pages/all/EveryWhere.tsx
================================================
import React from 'react';
export default function EveryWhere() {
return (
Everywhere
This component is rendered on every page of the store front.
You can modify this component at{' '}
`themes/sample/src/pages/all/EveryWhere.tsx`
You can also remove this by disabling the theme `sample`.
## Introduction
EverShop is a modern, TypeScript-first eCommerce platform built with GraphQL and React. Designed for developers, it offers essential commerce features in a modular, fully customizable architecture—perfect for building tailored shopping experiences with confidence and speed.
## Installation Using Docker
You can get started with EverShop in minutes by using the Docker image. The Docker image is a great way to get started with EverShop without having to worry about installing dependencies or configuring your environment.
```bash
curl -sSL https://raw.githubusercontent.com/evershopcommerce/evershop/main/docker-compose.yml > docker-compose.yml
docker-compose up -d
```
For the full installation guide, please refer to our [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide).
## Documentation
- [Installation guide](https://evershop.io/docs/development/getting-started/installation-guide).
- [Extension development](https://evershop.io/docs/development/module/create-your-first-extension).
- [Theme development](https://evershop.io/docs/development/theme/theme-overview).
## Demo
Explore our demo store.
Demo user:
Email: demo@evershop.io
Password: 123456
## Support
If you like my work, feel free to:
- ⭐ this repository. It helps.
- [][tweet] about EverShop. Thank you!
[tweet]: https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2Fevershopcommerce%2Fevershop&text=Awesome%20React%20Ecommerce%20Project&hashtags=react,ecommerce,expressjs,graphql
## Contributing
EverShop is an open-source project. We are committed to a fully transparent development process and appreciate highly any contributions. Whether you are helping us fix bugs, proposing new features, improving our documentation or spreading the word - we would love to have you as part of the EverShop community.
### Ask a question about EverShop
You can ask questions, and participate in discussions about EverShop-related topics in the EverShop Discord channel.
### Create a bug report
If you see an error message or run into an issue, please [create bug report](https://github.com/evershopcommerce/evershop/issues/new). This effort is valued and it will help all EverShop users.
### Submit a feature request
If you have an idea, or you're missing a capability that would make development easier and more robust, please [Submit feature request](https://github.com/evershopcommerce/evershop/issues/new).
If a similar feature request already exists, don't forget to leave a "+1".
If you add some more information such as your thoughts and vision about the feature, your comments will be embraced warmly :)
Please refer to our [Contribution Guidelines](./CONTRIBUTING.md) and [Code of Conduct](./CODE_OF_CONDUCT.md).
## License
[GPL-3.0 License](https://github.com/evershopcommerce/evershop/blob/main/LICENSE)
================================================
FILE: packages/evershop/package.json
================================================
{
"name": "@evershop/evershop",
"version": "2.1.1",
"type": "module",
"description": "The React Ecommerce platform. Built with Typescript, React and Postgres. Open-source and free. Fast and customizable.",
"files": [
"dist",
"src",
".swcrc"
],
"bin": {
"evershop": "./dist/bin/evershop.js"
},
"exports": {
"./types/*": {
"types": "./dist/types/*.d.ts"
},
"./lib/helpers": {
"import": "./dist/lib/helpers.js",
"types": "./dist/lib/helpers.d.ts"
},
"./lib/mail/*": {
"import": "./dist/lib/mail/*.js",
"types": "./dist/lib/mail/*.d.ts"
},
"./lib/util/*": {
"import": "./dist/lib/util/*.js",
"types": "./dist/lib/util/*.d.ts"
},
"./lib/event": {
"import": "./dist/lib/event/emitter.js",
"types": "./dist/lib/event/emitter.d.ts"
},
"./lib/event/subscriber": {
"import": "./dist/lib/event/subscriber.js",
"types": "./dist/lib/event/subscriber.d.ts"
},
"./lib/postgres": {
"import": "./dist/lib/postgres/connection.js",
"types": "./dist/lib/postgres/connection.d.ts"
},
"./lib/locale/*": {
"import": "./dist/lib/locale/*.js",
"types": "./dist/lib/locale/*.d.ts"
},
"./lib/log": {
"import": "./dist/lib/log/logger.js",
"types": "./dist/lib/log/logger.d.ts"
},
"./lib/router": {
"import": "./dist/lib/router/index.js",
"types": "./dist/lib/router/index.d.ts"
},
"./lib/widget": {
"import": "./dist/lib/widget/widgetManager.js",
"types": "./dist/lib/widget/widgetManager.d.ts"
},
"./lib/cronjob": {
"import": "./dist/lib/cronjob/jobManager.js",
"types": "./dist/lib/cronjob/jobManager.d.ts"
},
"./lib/middleware/delegate": {
"import": "./dist/lib/middleware/delegate.js",
"types": "./dist/lib/middleware/delegate.d.ts"
},
"./components/common/*": {
"import": "./dist/components/common/*.js",
"types": "./dist/components/common/*.d.ts"
},
"./components/admin/*": {
"import": "./dist/components/admin/*.js",
"types": "./dist/components/admin/*.d.ts"
},
"./components/frontStore/*": {
"import": "./dist/components/frontStore/*.js",
"types": "./dist/components/frontStore/*.d.ts"
},
"./graphql/services": {
"import": "./dist/modules/graphql/services/index.js",
"types": "./dist/modules/graphql/services/index.d.ts"
},
"./catalog/services": {
"import": "./dist/modules/catalog/services/index.js",
"types": "./dist/modules/catalog/services/index.d.ts"
},
"./customer/services": {
"import": "./dist/modules/customer/services/index.js",
"types": "./dist/modules/customer/services/index.d.ts"
},
"./setting/services": {
"import": "./dist/modules/setting/services/index.js",
"types": "./dist/modules/setting/services/index.d.ts"
},
"./checkout/services": {
"import": "./dist/modules/checkout/services/index.js",
"types": "./dist/modules/checkout/services/index.d.ts"
},
"./oms/services": {
"import": "./dist/modules/oms/services/index.js",
"types": "./dist/modules/oms/services/index.d.ts"
},
"./cms/services": {
"import": "./dist/modules/cms/services/index.js",
"types": "./dist/modules/cms/services/index.d.ts"
},
"./package.json": "./package.json"
},
"scripts": {
"prepack": "rimraf dist && tsc && copyfiles -u 1 \"src/**/*.{graphql,scss,css,json}\" dist",
"dev": "node ./dist/bin/dev/index.js",
"start": "node ./dist/bin/start/index.js",
"build": "node ./dist/bin/build/index.js",
"build-fast": "evershop build -- --skip-minify",
"user:create": "evershop user:create",
"user:changePassword": "evershop user:changePassword",
"test": "jest"
},
"author": "The Nguyen (https://evershop.io)",
"license": "GNU GENERAL PUBLIC LICENSE 3.0",
"repository": {
"type": "git",
"url": "git+https://github.com/evershopcommerce/evershop.git"
},
"keywords": [
"ecommerce",
"shopping cart",
"cart"
],
"bugs": {
"url": "https://github.com/evershopcommerce/evershop/issues"
},
"homepage": "http://evershop.io/",
"dependencies": {
"@base-ui/react": "^1.1.0",
"@ckeditor/ckeditor5-build-classic": "^36.0.1",
"@ckeditor/ckeditor5-react": "^5.1.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@editorjs/editorjs": "^2.30.8",
"@editorjs/header": "^2.8.7",
"@editorjs/list": "^1.10.0",
"@editorjs/quote": "^2.6.0",
"@editorjs/raw": "^2.5.0",
"@evershop/editorjs-image": "^1.1.0",
"@evershop/postgres-query-builder": "^2.0.1",
"@graphql-tools/load-files": "^6.6.1",
"@graphql-tools/merge": "^8.4.2",
"@graphql-tools/schema": "^9.0.19",
"@hapi/topo": "^5.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@stripe/react-stripe-js": "^1.5.0",
"@stripe/stripe-js": "^1.18.0",
"@swc/cli": "^0.7.7",
"@swc/core": "^1.11.29",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.13",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
"autoprefixer": "^10.4.13",
"axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.0",
"boxen": "^5.1.2",
"class-variance-authority": "^0.7.1",
"clean-css": "^5.3.1",
"clsx": "^2.1.1",
"config": "^3.3.6",
"connect-pg-simple": "^9.0.0",
"cookie-parser": "^1.4.6",
"cross-spawn": "^7.0.6",
"css-loader": "^6.7.1",
"csv-parser": "^3.0.0",
"dayjs": "^1.10.6",
"debug": "^4.3.2",
"dotenv": "^16.3.1",
"enquirer": "^2.3.6",
"execa": "^9.6.0",
"express": "^4.21.2",
"express-session": "^1.17.3",
"fast-glob": "^3.3.3",
"flatpickr": "^4.6.9",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.8",
"html-entities": "^2.3.3",
"html-webpack-plugin": "^5.5.0",
"immer": "^10.1.1",
"jsesc": "^3.0.2",
"json5": "^2.2.1",
"jsonwebtoken": "^9.0.2",
"kleur": "3.0.3",
"lodash.isequalwith": "^4.4.0",
"lucide-react": "^0.562.0",
"luxon": "^2.0.2",
"mini-css-extract-plugin": "^2.6.1",
"minimatch": "^10.2.3",
"multer": "^2.1.1",
"node-cron": "^3.0.3",
"ora": "^5.4.1",
"pg": "^8.16.3",
"postcss": "^8.4.18",
"postcss-loader": "^8.2.0",
"prop-types": "^15.8.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-hook-form": "^7.61.1",
"react-refresh": "^0.14.0",
"react-select": "^5.4.0",
"react-slick": "^0.31.0",
"react-toastify": "^6.2.0",
"recharts": "^2.0.9",
"sanitize-html": "^2.17.0",
"sass": "^1.53.0",
"sass-loader": "^13.0.2",
"semver": "^7.6.3",
"serve-static": "^1.15.0",
"session-file-store": "^1.5.0",
"sharp": "^0.33.5",
"slick-carousel": "^1.8.1",
"stripe": "^8.176.0",
"style-loader": "^3.3.1",
"swc-minify-webpack-plugin": "^2.1.3",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"touch": "^3.1.1",
"tw-animate-css": "^1.4.0",
"uniqid": "^5.3.0",
"urql": "^3.0.3",
"uuid": "^9.0.0",
"webpack": "^5.72.1",
"webpack-dev-middleware": "^7.4.2",
"webpack-hot-middleware": "^2.26.1",
"webpackbar": "^5.0.2",
"winston": "^3.3.3",
"yargs": "^17.7.2",
"zero-decimal-currencies": "^1.2.0"
},
"devDependencies": {
"@parcel/watcher": "^2.5.1",
"@paypal/paypal-js": "^8.4.2",
"@types/config": "^3.3.5",
"@types/express": "^5.0.1",
"@types/express-session": "^1.18.2",
"@types/multer": "^2.0.0",
"@types/node": "^22.14.1",
"@types/pg": "^8.15.2",
"@types/react": "^19.1.2",
"@types/sanitize-html": "^2.16.0",
"copyfiles": "^2.4.1",
"typescript": "^5.8.3"
}
}
================================================
FILE: packages/evershop/scripts/postpack.js
================================================
import fs from 'fs';
import path from 'path';
import packageJson from '../package.json' with { type: 'json' };
// Get the current version of the package from the nearest package.json file
const { version } = packageJson;
// Get the --pack-destination from the command line arguments
// Create a package.json file in the packDestination directory with dependencies is the package itself
fs.writeFileSync(
path.resolve(process.env.npm_config_pack_destination, 'package.json'),
JSON.stringify(
{
name: packageJson.name,
version,
dependencies: {
'@evershop/evershop': `file:./evershop-evershop-${version}.tgz`
},
scripts: {
setup: 'evershop install',
start: 'evershop start',
'start:debug': 'evershop start:debug',
build: 'evershop build',
dev: 'evershop dev',
'user:create': 'evershop user:create'
}
},
null,
2
)
);
================================================
FILE: packages/evershop/scripts/postpublish.js
================================================
import fs from 'fs';
import path from 'path';
function getFileRecursive(dir, files) {
const list = fs.readdirSync(dir);
list.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
getFileRecursive(filePath, files);
} else {
files.push(filePath);
}
});
}
const files = [];
getFileRecursive(path.resolve(__dirname, './bin/serve'), files);
files.forEach((file) => {
const source = fs.readFileSync(file, { encoding: 'utf8', flag: 'r' });
const result = source.replace(/\.\.\/dist/g, '../src');
fs.writeFileSync(file, result, 'utf8');
});
================================================
FILE: packages/evershop/scripts/prepublish.js
================================================
import fs from 'fs';
import path from 'path';
fs.copyFile(
path.resolve(__dirname, '../../README.md'),
path.resolve(__dirname, './README.md'),
(err) => {
if (err) throw err;
}
);
================================================
FILE: packages/evershop/src/bin/build/client/index.js
================================================
import webpack from 'webpack';
import { error } from '../../../src/lib/log/logger';
import { createConfigClient } from '../../../src/lib/webpack/prod/createConfigClient';
export async function buildClient(routes) {
const config = createConfigClient(routes);
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err || stats.hasErrors()) {
error(
stats.toString({
errorDetails: true,
warnings: true
})
);
reject(err);
}
resolve(stats);
});
});
}
================================================
FILE: packages/evershop/src/bin/build/complie.js
================================================
import pkg from 'webpack';
import { error } from '../../lib/log/logger.js';
import { createConfigClient } from '../../lib/webpack/prod/createConfigClient.js';
import { createConfigServer } from '../../lib/webpack/prod/createConfigServer.js';
const { webpack } = pkg;
export async function compile(routes) {
const config = [createConfigClient(routes), createConfigServer(routes)];
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err || stats.hasErrors()) {
if (err) {
error(err);
}
error(
stats.toString({
errorDetails: true,
warnings: true
})
);
reject(err);
}
resolve(stats);
});
});
}
================================================
FILE: packages/evershop/src/bin/build/index.js
================================================
import { existsSync, mkdirSync, rmSync } from 'fs';
import path from 'path';
import config from 'config';
import { CONSTANTS } from '../../lib/helpers.js';
import { error } from '../../lib/log/logger.js';
import { loadModuleRoutes } from '../../lib/router/loadModuleRoutes.js';
import { getRoutes } from '../../lib/router/Router.js';
import { lockHooks } from '../../lib/util/hookable.js';
import { lockRegistry } from '../../lib/util/registry.js';
import { validateConfiguration } from '../../lib/util/validateConfiguration.js';
import { isBuildRequired } from '../../lib/webpack/isBuildRequired.js';
import { getEnabledExtensions } from '../extension/index.js';
import { loadBootstrapScript } from '../lib/bootstrap/bootstrap.js';
import { buildEntry } from '../lib/buildEntry.js';
import { getCoreModules } from '../lib/loadModules.js';
import { compile } from './complie.js';
import './initEnvBuild.js';
/* Loading modules and initilize routes, components */
const modules = [...getCoreModules(), ...getEnabledExtensions()];
/** Loading routes */
modules.forEach((module) => {
try {
// Load routes
loadModuleRoutes(module.path);
} catch (e) {
error(e);
process.exit(0);
}
});
/** Clean up the build directory */
if (existsSync(path.resolve(CONSTANTS.BUILDPATH))) {
// Delete directory recursively
rmSync(path.resolve(CONSTANTS.BUILDPATH), { recursive: true });
mkdirSync(path.resolve(CONSTANTS.BUILDPATH));
} else {
mkdirSync(path.resolve(CONSTANTS.BUILDPATH), { recursive: true });
}
export default async function build() {
/** Loading bootstrap script from modules */
try {
for (const module of modules) {
await loadBootstrapScript(module, {
command: 'build',
env: 'production',
process: 'main'
});
}
lockHooks();
lockRegistry();
// Get the configuration (nodeconfig)
validateConfiguration(config);
} catch (e) {
error(e);
process.exit(1);
}
process.env.ALLOW_CONFIG_MUTATIONS = false;
const routes = getRoutes();
await buildEntry(routes.filter((r) => isBuildRequired(r)));
/** Build */
await compile(routes);
}
process.on('uncaughtException', function (exception) {
import('../../lib/log/logger.js').then((module) => {
module.error(exception);
});
});
process.on('unhandledRejection', (reason, p) => {
import('../../lib/log/logger.js').then((module) => {
module.error(`Unhandled Rejection: ${reason} at: ${p}`);
});
});
build();
================================================
FILE: packages/evershop/src/bin/build/initEnvBuild.ts
================================================
import 'dotenv/config';
process.env.NODE_ENV = 'production';
process.env.ALLOW_CONFIG_MUTATIONS = 'true';
================================================
FILE: packages/evershop/src/bin/build/server/index.js
================================================
import pkg from 'webpack';
import { error } from '../../../src/lib/log/logger.js';
import { createConfigServer } from '../../../src/lib/webpack/prod/createConfigServer.js';
const { webpack } = pkg;
export const buildServer = async function buildServer(routes) {
const config = createConfigServer(routes);
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err || stats.hasErrors()) {
error(
stats.toString({
errorDetails: true,
warnings: true
})
);
reject(err);
}
resolve(stats);
});
});
};
================================================
FILE: packages/evershop/src/bin/build/server/useDDL.js
================================================
import { existsSync, rmdirSync } from 'fs';
import { mkdir, writeFile } from 'fs/promises';
import path from 'path';
import { inspect } from 'util';
import boxen from 'boxen';
import { green, red } from 'kleur';
import ora from 'ora';
import pkg from 'webpack';
import { getComponentsByRoute } from '../../../src/lib/componee/getComponentByRoute.js';
import { CONSTANTS } from '../../../src/lib/helpers.js';
import { info } from '../../../src/lib/log/logger.js';
import { getRoutes } from '../../../src/lib/router/routes.js';
// Run building vendor first
import { createVendorConfig } from '../../../src/lib/webpack/configProvider.js';
import { loadModuleComponents } from '../../serve/loadModuleComponents.js';
import { loadModuleRoutes } from '../../serve/loadModuleRoutes.js';
import { loadModules } from '../../serve/loadModules.js';
const { webpack } = pkg;
const modules = loadModules(path.resolve(__dirname, '../../../src', 'modules'));
const spinner = ora({
text: green('Starting server build'),
spinner: 'dots12'
}).start();
spinner.start();
// Initilizing routes
modules.forEach((module) => {
try {
// Load routes
loadModuleRoutes(module.path);
} catch (e) {
spinner.fail(`${red(e.stack)}\n`);
process.exit(0);
}
});
// Initializing components
modules.forEach((module) => {
try {
// Load components
loadModuleComponents(module.path);
} catch (e) {
spinner.fail(`${red(e.stack)}\n`);
process.exit(0);
}
});
const routes = getRoutes();
// Collect all "controller" route
const controllers = routes.filter((r) => r.isApi === false);
const promises = [];
const total = controllers.length - 1;
let completed = 0;
spinner.text = `Start building ☕☕☕☕☕\n${Array(total).fill('▒').join('')}`;
if (existsSync(path.resolve(CONSTANTS.ROOTPATH, './.evershop/build'))) {
rmdirSync(path.resolve(CONSTANTS.ROOTPATH, './.evershop/build'), {
recursive: true
});
}
const start = Date.now();
const vendorComplier = webpack(createVendorConfig(webpack));
const webpackVendorPromise = new Promise((resolve, reject) => {
vendorComplier.run((err, stats) => {
if (err) {
reject(err);
} else if (stats.hasErrors()) {
reject(
new Error(
stats.toString({
errorDetails: true,
warnings: true
})
)
);
} else {
resolve(stats);
}
});
});
webpackVendorPromise.then(async () => {
controllers.forEach((route) => {
const buildFunc = async function () {
const components = getComponentsByRoute(route.id);
if (!components) {
return;
}
Object.keys(components).forEach((area) => {
Object.keys(components[area]).forEach((id) => {
components[area][
id
].component = `---require("${components[area][id].source}")---`;
delete components[area][id].source;
});
});
const buildPath =
route.isAdmin === true
? `./admin/${route.id}`
: `./frontStore/${route.id}`;
let content = `var components = module.exports = exports = ${inspect(
components,
{ depth: 5 }
)
.replace(/'---/g, '')
.replace(/---'/g, '')}`;
content += '\r\n';
await mkdir(
path.resolve(CONSTANTS.ROOTPATH, './.evershop/build', buildPath),
{ recursive: true }
);
await writeFile(
path.resolve(
CONSTANTS.ROOTPATH,
'.evershop/build',
buildPath,
'components.js'
),
content
);
const name =
route.isAdmin === true ? `admin/${route.id}` : `frontStore/${route.id}`;
const entry = {};
entry[name] = [
path.resolve(
CONSTANTS.ROOTPATH,
'.evershop',
'build',
buildPath,
'components.js'
),
path.resolve(
CONSTANTS.LIBPATH,
'../components/common/react/server',
'render.js'
)
];
const compiler = webpack({
mode: 'production', // "production" | "development" | "none"
module: {
rules: [
{
test: /\/views|components|context\/(.*).js?$/,
// test: /\.js?$/,
exclude: /(bower_components)/,
use: {
loader: 'babel-loader?cacheDirectory',
options: {
sourceType: 'unambiguous',
cacheDirectory: true,
presets: [
[
'@babel/preset-env',
{
exclude: [
'@babel/plugin-transform-regenerator',
'@babel/plugin-transform-async-to-generator'
]
}
],
'@babel/preset-react'
]
}
}
},
{
test: /getComponents\.js/,
use: [
{
loader: path.resolve(
CONSTANTS.LIBPATH,
'webpack/getComponentLoader.js'
),
options: {
componentsPath: path.resolve(
CONSTANTS.ROOTPATH,
'./.evershop/build',
buildPath,
'components.js'
)
}
}
]
}
]
},
// name: 'main',
target: 'node12.18',
entry,
output: {
path: path.resolve(
CONSTANTS.ROOTPATH,
'./.evershop/build',
buildPath,
'server'
),
libraryTarget: 'commonjs2',
globalObject: 'this',
filename: 'index.js'
},
resolve: {
alias: {
react: path.resolve(CONSTANTS.NODEMODULEPATH, 'react')
}
},
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(
CONSTANTS.ROOTPATH,
'./.evershop/build/vendor-manifest.json'
)
})
]
});
const webpackPromise = new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
reject(err);
} else if (stats.hasErrors()) {
reject(
new Error(
stats.toString({
errorDetails: true,
warnings: true
})
)
);
} else {
resolve(stats);
}
});
});
await webpackPromise;
completed += 1;
spinner.text = `Start building ☕☕☕☕☕\n${Array(completed)
.fill(green('█'))
.concat(total - completed > 0 ? Array(total - completed).fill('▒') : [])
.join('')}`;
};
promises.push(buildFunc());
});
await Promise.all(promises)
.then(() => {
spinner.succeed(
green('Building completed!!!\n') +
boxen(green('Please run "npm run start" to start your website'), {
title: 'EverShop',
titleAlignment: 'center',
padding: 1,
margin: 1,
borderColor: 'green'
})
);
const end = Date.now();
info(`Execution time: ${end - start} ms`);
process.exit(0);
})
.catch((e) => {
spinner.fail(`${red(e)}\n`);
process.exit(0);
});
});
================================================
FILE: packages/evershop/src/bin/build/server/useVendorChunk.js
================================================
import { existsSync, rmSync } from 'fs';
import path from 'path';
import { green, red } from 'kleur';
import ora from 'ora';
import webpack from 'webpack';
import { CONSTANTS } from '../../../src/lib/helpers.js';
import { getRoutes } from '../../../src/lib/router/routes.js';
import { createConfig } from '../../../src/lib/webpack/createConfig.js';
import { loadModuleComponents } from '../../serve/loadModuleComponents.js';
import { loadModuleRoutes } from '../../serve/loadModuleRoutes.js';
import { loadModules } from '../../serve/loadModules.js';
import { createComponents } from '../createComponents.js';
(async () => {
const start = Date.now();
const modules = loadModules(
path.resolve(__dirname, '../../../src', 'modules')
);
const spinner = ora({
text: green('Starting server build'),
spinner: 'dots12'
}).start();
spinner.start();
/** Initilizing routes */
modules.forEach((module) => {
try {
// Load routes
loadModuleRoutes(module.path);
} catch (e) {
spinner.fail(`${red(e.stack)}\n`);
process.exit(0);
}
});
/** Initializing components */
modules.forEach((module) => {
try {
// Load components
loadModuleComponents(module.path);
} catch (e) {
spinner.fail(`${red(e.stack)}\n`);
process.exit(0);
}
});
/** Get list of routes */
const routes = getRoutes();
/** Collect all 'controller' routes */
const controllers = routes.filter((r) => r.isApi === false);
/** Clean up the build directory */
if (existsSync(CONSTANTS.BUILDPATH)) {
rmSync(CONSTANTS.BUILDPATH, { recursive: true });
}
/** Create components.js file for each route */
await createComponents(controllers);
/** Create the webpack complier object */
const compiler = webpack(createConfig(true, controllers));
/** Run the build */
await new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) {
reject(err);
} else if (stats.hasErrors()) {
reject(
new Error(
stats.toString({
errorDetails: true,
warnings: true
})
)
);
} else {
resolve(stats);
}
});
});
const end = Date.now();
spinner.succeed(`${green('Server build completed in')} ${end - start}ms`);
process.exit(0);
})();
================================================
FILE: packages/evershop/src/bin/dev/compileTs.js
================================================
import path from 'path';
import { compileSwc } from '../lib/watch/compileSwc.js';
import { getSrcPaths } from '../lib/watch/getSrcPaths.js';
async function compileTs() {
const srcPaths = getSrcPaths();
const events = srcPaths.map((srcPath) => {
return {
srcPath: srcPath,
distPath: path.resolve(srcPath, '..', 'dist')
};
});
await Promise.all(
events.map((event) => {
return compileSwc(event.srcPath, event.distPath);
})
);
}
export { compileTs };
================================================
FILE: packages/evershop/src/bin/dev/enableWatcher.js
================================================
import { subscribe } from '@parcel/watcher';
import { CONSTANTS } from '../../lib/helpers.js';
import { watchHandler } from '../lib/watch/watchHandler.js';
export default async function enableWatcher() {
const watcherInstance = await subscribe(
CONSTANTS.ROOTPATH,
(err, events) => {
if (err) {
return;
}
watchHandler(events);
},
{
ignore: [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/.git/**',
'**/.cache/**',
'**/.next/**',
'**/.nuxt/**',
'**/.vscode/**'
]
}
);
process.on('SIGINT', () => {
watcherInstance.unsubscribe();
process.exit(0);
});
process.on('SIGTERM', () => {
watcherInstance.unsubscribe();
});
process.on('exit', () => {
watcherInstance.unsubscribe();
});
}
================================================
FILE: packages/evershop/src/bin/dev/hooks.js
================================================
import { isBuiltin } from 'node:module';
import path, { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
let broadcastChannel;
export function initialize(data) {
broadcastChannel = data.broadcastChannel;
}
export function resolve(specifier, context, nextResolve) {
if (
isBuiltin(specifier) ||
specifier.includes('?t=') ||
context.parentURL === undefined
) {
return nextResolve(specifier, context);
} else {
const modulePath = !specifier.startsWith('file:')
? path.resolve(dirname(fileURLToPath(context.parentURL)), specifier)
: fileURLToPath(specifier);
if (modulePath.includes('node_modules')) {
return nextResolve(specifier, context);
} else {
broadcastChannel.postMessage({
path: modulePath,
});
return nextResolve(specifier, context);
}
}
}
================================================
FILE: packages/evershop/src/bin/dev/index.ts
================================================
import path from 'path';
import { fileURLToPath } from 'url';
import spawn from 'cross-spawn';
import { debug, error } from '../../lib/log/logger.js';
function startDev() {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = [path.resolve(__dirname, 'init.js')];
const appProcess = spawn('node', args, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: {
...process.env,
ALLOW_CONFIG_MUTATIONS: true
}
});
appProcess.on('error', (err) => {
error(`Error spawning processor: ${err}`);
});
appProcess.on('message', (message) => {
debug('Restarting the development server');
if (message === 'RESTART_ME') {
if (appProcess && appProcess.pid) {
appProcess.removeAllListeners();
appProcess.kill('SIGTERM');
}
startDev();
}
});
return appProcess;
}
const childProcess = startDev();
process.on('exit', () => {
// Cleanup child processes on exit
if (childProcess && childProcess.pid) {
childProcess.kill();
}
});
================================================
FILE: packages/evershop/src/bin/dev/init.ts
================================================
import './register.js';
import './initEnvDev.js';
import { debug, error } from '../../lib/log/logger.js';
import { start } from '../lib/startUp.js';
import { compileTs } from './compileTs.js';
import enableWatcher from './enableWatcher.js';
await compileTs();
enableWatcher();
start({
command: 'dev',
env: 'development',
process: 'main'
});
process.on('SIGTERM', async () => {
debug('Received SIGTERM, shutting down the main process...');
try {
process.exit(0);
} catch (err) {
error('Error during shutdown the main process:');
error(err);
process.exit(1);
}
});
process.on('uncaughtException', function (exception) {
import('../../lib/log/logger.js').then((module) => {
module.error(exception);
});
});
process.on('unhandledRejection', (reason, p) => {
import('../../lib/log/logger.js').then((module) => {
module.error(`Unhandled Rejection: ${reason} at: ${p}`);
});
});
================================================
FILE: packages/evershop/src/bin/dev/initEnvDev.ts
================================================
import 'dotenv/config';
process.env.NODE_ENV = 'development';
process.env.ALLOW_CONFIG_MUTATIONS = 'true';
================================================
FILE: packages/evershop/src/bin/dev/register.js
================================================
import { register } from 'node:module';
import { MessageChannel } from 'node:worker_threads';
export const maps = new Map();
const { port1: listenChannel, port2: broadcastChannel } = new MessageChannel();
listenChannel.on('message', (message) => {
maps.set(message.path, true);
});
register('./hooks.js', {
parentURL: import.meta.url,
data: { broadcastChannel },
transferList: [broadcastChannel],
});
export function has(pathName) {
return maps.has(pathName);
}
================================================
FILE: packages/evershop/src/bin/evershop.js
================================================
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const { argv } = yargs(hideBin(process.argv));
const command = argv._[0];
try {
if (command === 'build') {
await import('./build/index.js');
} else if (command === 'dev') {
await import('./dev/index.js');
} else if (command === 'start') {
await import('./start/index.js');
} else if (command === 'install') {
await import('./install/index.js');
} else if (command === 'user:create') {
await import('./user/create.js');
} else if (command === 'user:changePassword') {
await import('./user/changePassword.js');
} else if (command === 'theme:active') {
await import('./theme/active.js');
} else if (command === 'theme:twizz') {
await import('./theme/twizz.js');
} else if (command === 'theme:create') {
await import('./theme/create.js');
} else if (command === 'seed') {
await import('./seed/index.js');
} else {
throw new Error('Invalid command');
}
} catch (e) {
import('../lib/log/logger.js').then((module) => {
module.error(e);
});
}
process.on('uncaughtException', function (exception) {
import('../lib/log/logger.js').then((module) => {
module.error(exception);
});
});
process.on('unhandledRejection', (reason, p) => {
import('../lib/log/logger.js').then((module) => {
module.error(`Unhandled Rejection: ${reason} at: ${p}`);
});
});
================================================
FILE: packages/evershop/src/bin/extension/index.ts
================================================
import { existsSync } from 'fs';
import { resolve } from 'path';
import { CONSTANTS } from '../../lib/helpers.js';
import { error, warning } from '../../lib/log/logger.js';
import { getConfig } from '../../lib/util/getConfig.js';
import { isDevelopmentMode } from '../../lib/util/isDevelopmentMode.js';
import { isProductionMode } from '../../lib/util/isProductionMode.js';
import { Extension } from '../../types/extension.js';
import { getCoreModules } from '../lib/loadModules.js';
let extensions: Extension[] | undefined = undefined;
function loadExtensions(): Extension[] {
const coreModules = getCoreModules();
const list = getConfig('system.extensions', []) as Extension[];
const extensions: Extension[] = [];
list.forEach((extension) => {
if (
coreModules.find((module) => module.name === extension.name) ||
extensions.find((e) => e.name === extension.name)
) {
throw new Error(
`Extension ${extension.name} is invalid. extension name must be unique.`
);
}
if (extension.enabled !== true) {
warning(`Extension ${extension.name} is not enabled. Skipping.`);
return;
}
if (!existsSync(extension.resolve)) {
warning(
`Extension ${extension.name} has resolve path ${extension.resolve} which does not exist. Skipping.`
);
return;
}
if (isProductionMode() || extension.resolve.includes('node_modules')) {
// Make sure the folder has 'dist' subdirectory
if (!existsSync(resolve(extension.resolve, 'dist'))) {
error(
`Extension '${
extension.name
}' must have a 'dist' directory at ${resolve(
extension.resolve,
'dist'
)}. This is required for production mode.`
);
process.exit(1);
} else {
extensions.push({
...extension,
path: resolve(CONSTANTS.ROOTPATH, extension.resolve, 'dist')
});
}
}
if (isDevelopmentMode() && !extension.resolve.includes('node_modules')) {
// Make sure the folder has 'src' subdirectory
if (!existsSync(resolve(extension.resolve, 'src'))) {
error(
`Extension '${
extension.name
}' must have a 'src' directory at ${resolve(
extension.resolve,
'src'
)}`
);
process.exit(1);
} else {
extensions.push({
...extension,
srcPath: resolve(extension.resolve, 'src'),
path: resolve(extension.resolve, 'dist')
});
}
}
});
// Sort the extensions by priority, smaller number means higher priority
extensions.sort((a, b) => a.priority - b.priority);
return extensions;
}
export function getEnabledExtensions() {
if (extensions === undefined) {
extensions = loadExtensions();
}
return extensions;
}
================================================
FILE: packages/evershop/src/bin/install/createMigrationTable.js
================================================
import { execute } from '@evershop/postgres-query-builder';
export async function createMigrationTable(connection) {
await execute(
connection,
`CREATE TABLE IF NOT EXISTS "migration" (
"migration_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY,
"module" varchar NOT NULL,
"version" varchar NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MODULE_UNIQUE" UNIQUE ("module")
)`
);
}
================================================
FILE: packages/evershop/src/bin/install/index.js
================================================
import { mkdir, writeFile } from 'fs/promises';
import path from 'path';
import {
commit,
execute,
insertOnUpdate,
rollback,
startTransaction
} from '@evershop/postgres-query-builder';
import boxen from 'boxen';
import enquirer from 'enquirer';
import kleur from 'kleur';
import ora from 'ora';
import { Pool } from 'pg';
import { CONSTANTS } from '../../lib/helpers.js';
import { error, success } from '../../lib/log/logger.js';
import { hashPassword } from '../../lib/util/passwordHelper.js';
import { migrate } from '../lib/bootstrap/migrate.js';
import { getCoreModules } from '../lib/loadModules.js';
// The installation command will create a .env file in the root directory of the project.
// If you are using docker, do not run this command. Instead, you should set the environment variables in the docker-compose.yml file and run `npm run start`
// This command means for the developer who want to install the system on their local machine.
async function install() {
// Check if the env for database is set
if (process.env.DB_HOST) {
error(
'We found that you have already set the environment variables for the database. Look like you have already installed the system. Run `npm run build` and `npm run start` to launch your store.'
);
process.exit(0);
}
var db;
var adminUser;
// eslint-disable-next-line no-console
console.log(
kleur.green(
boxen('Welcome to EverShop - The open-source e-commerce platform', {
title: 'EverShop',
titleAlignment: 'center',
padding: 1,
margin: 1,
borderColor: 'green'
})
)
);
const dbQuestions = [
{
type: 'input',
name: 'databaseHost',
message: 'Postgres Database Host (localhost)',
initial: process.env.DB_HOST || 'localhost',
skip: !!process.env.DB_HOST
},
{
type: 'input',
name: 'databasePort',
message: 'Postgres Database Port (5432)',
initial: process.env.DB_PORT || 5432,
skip: !!process.env.DB_PORT
},
{
type: 'input',
name: 'databaseName',
message: 'Postgres Database Name (evershop)',
initial: process.env.DB_NAME || 'evershop',
skip: !!process.env.DB_NAME
},
{
type: 'input',
name: 'databaseUser',
message: 'Postgres Database User (postgres)',
initial: process.env.DB_USER || 'postgres',
skip: !!process.env.DB_USER
},
{
type: 'input',
name: 'databasePassword',
message: 'PostgreSQL Database Password ()',
initial: process.env.DB_PASSWORD || '',
skip: !!process.env.DB_PASSWORD
}
];
try {
db = await enquirer.prompt(dbQuestions);
} catch (e) {
process.exit(0);
}
const baseDBSetting = {
host: db.databaseHost,
port: db.databasePort,
user: db.databaseUser,
password: db.databasePassword,
database: db.databaseName,
max: 10,
idleTimeoutMillis: 30000
};
// We will try with SSL option enabled first
let pool = new Pool({ ...baseDBSetting, ssl: true });
let sslMode;
// Test the secure connection
try {
await pool.query(`SELECT 1`);
sslMode = 'require';
} catch (e) {
if (e.message.includes('does not support SSL')) {
// If the database does not support SSL, we will try to connect without SSL
pool = new Pool({ ...baseDBSetting, ssl: false });
sslMode = 'disable';
} else if (e.message.includes('certificate')) {
error(
`Looks like your database server does not have a valid SSL certificate. Please turn off the SSL option in the database configuration, restart the database server and try again.`
);
} else {
error(e);
process.exit(0);
}
}
// Check postgres database version
try {
const { rows } = await execute(pool, `SHOW SERVER_VERSION;`);
if (rows[0].server_version < '13.0') {
error(
`Your database server version(${rows[0].server_version}) is not supported. Please upgrade to PostgreSQL version 13.0 or higher`
);
process.exit(0);
}
} catch (e) {
error(e);
process.exit(0);
}
const adminUserQuestions = [
{
type: 'input',
name: 'fullName',
message: 'Your full name',
initial: process.env.ADMIN_FULLNAME || '',
skip: !!process.env.ADMIN_FULLNAME
},
{
type: 'input',
name: 'email',
message: 'Your administrator user email',
initial: process.env.ADMIN_EMAIL || 'admin@admin.com',
skip: !!process.env.ADMIN_EMAIL,
validate: (value) => {
if (
!value.match(
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
)
) {
return 'Invalid email';
}
return true;
}
},
{
type: 'password',
name: 'password',
message: 'Your administrator user password',
initial: process.env.ADMIN_PASSWORD || '123456',
skip: !!process.env.ADMIN_PASSWORD,
validate: (value) => {
if (value.length < 8) {
return 'Your password must be at least 8 characters.';
}
if (value.search(/[a-z]/i) < 0) {
return 'Your password must contain at least one letter.';
}
if (value.search(/[0-9]/) < 0) {
return 'Your password must contain at least one digit.';
}
return true;
}
}
];
try {
adminUser = await enquirer.prompt(adminUserQuestions);
} catch (e) {
process.exit(0);
}
/* Start installation */
const messages = [];
messages.push(`\n\n${kleur.green('EverShop is being installed ☕ ☕ ☕')}`);
messages.push('Creating .env file');
const spinner = ora({
text: kleur.green(messages.join('\n')),
spinner: 'dots12'
}).start();
spinner.start();
/** Create the .env file at the root folder with the database connection */
await writeFile(
path.resolve(CONSTANTS.ROOTPATH, '.env'),
`DB_HOST="${db.databaseHost}"
DB_PORT="${db.databasePort}"
DB_NAME="${db.databaseName}"
DB_USER="${db.databaseUser}"
DB_PASSWORD="${db.databasePassword}"
DB_SSLMODE="${sslMode}"
`
);
messages.pop();
messages.push(kleur.green('✔ Created .env file'));
spinner.text = messages.join('\n');
// Create `media` folder
await mkdir(path.resolve(CONSTANTS.ROOTPATH, 'media'), { recursive: true });
// Create `public` folder
await mkdir(path.resolve(CONSTANTS.ROOTPATH, 'public'), { recursive: true });
// Start install database
messages.push(kleur.green('Setting up a database'));
spinner.text = messages.join('\n');
const connection = await pool.connect();
await startTransaction(connection);
try {
// Create the admin user
const passwordHash = hashPassword(adminUser.password || '123456');
await execute(
connection,
`CREATE TABLE IF NOT EXISTS "admin_user" (
"admin_user_id" INT GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1) PRIMARY KEY,
"uuid" UUID NOT NULL DEFAULT gen_random_uuid (),
"status" boolean NOT NULL DEFAULT TRUE,
"email" varchar NOT NULL,
"password" varchar NOT NULL,
"full_name" varchar DEFAULT NULL,
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ADMIN_USER_EMAIL_UNIQUE" UNIQUE ("email"),
CONSTRAINT "ADMIN_USER_UUID_UNIQUE" UNIQUE ("uuid")
);`
);
await insertOnUpdate('admin_user', ['email'])
.given({
status: 1,
email: adminUser?.email || 'admin@evershop.io',
password: passwordHash,
full_name: adminUser?.fullName || 'Admin'
})
.execute(connection);
// Run module migrations
const coreModules = getCoreModules();
await migrate(coreModules, connection);
await commit(connection);
} catch (e) {
await rollback(connection);
error(e);
process.exit(0);
}
messages.pop();
messages.push(kleur.green('✔ Setup database'));
messages.push(kleur.green('✔ Create admin user'));
spinner.succeed(messages.join('\n'));
// eslint-disable-next-line no-console
console.log(
boxen(
kleur.green(
'Installation completed!. Run `npm run build` and `npm run start` to launch your store'
),
{
title: 'EverShop',
titleAlignment: 'center',
padding: 1,
margin: 1,
borderColor: 'green'
}
)
);
process.exit(0);
}
(async () => {
try {
await install();
} catch (e) {
error(e);
process.exit(0);
}
})();
================================================
FILE: packages/evershop/src/bin/install/templates/config.json
================================================
{
"shop": {
"currency": "USD",
"language": "en",
"weightUnit": "kg",
"timezone": "UTC"
},
"system": {
"database": {
"host": "localhost",
"port": 5432,
"database": "evershop",
"user": "admin",
"password": "123456"
}
}
}
================================================
FILE: packages/evershop/src/bin/lib/addDefaultMiddlewareFuncs.ts
================================================
import { select } from '@evershop/postgres-query-builder';
import sessionStorage from 'connect-pg-simple';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import pathToRegexp from 'path-to-regexp';
import { translate } from '../../lib/locale/translate/translate.js';
import { debug, warning } from '../../lib/log/logger.js';
import publicStatic from '../../lib/middlewares/publicStatic.js';
import themePublicStatic from '../../lib/middlewares/themePublicStatic.js';
import { pool } from '../../lib/postgres/connection.js';
import { getRoutes } from '../../lib/router/Router.js';
import { getConfig } from '../../lib/util/getConfig.js';
import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js';
import isProductionMode from '../../lib/util/isProductionMode.js';
import { getAdminSessionCookieName } from '../../modules/auth/services/getAdminSessionCookieName.js';
import { getCookieSecret } from '../../modules/auth/services/getCookieSecret.js';
import { getFrontStoreSessionCookieName } from '../../modules/auth/services/getFrontStoreSessionCookieName.js';
import { setPageMetaInfo } from '../../modules/cms/services/pageMetaInfo.js';
import { getDevMiddleware, getHotMiddleware } from './devEnvHelper.js';
export function addDefaultMiddlewareFuncs(app) {
app.use((request, response, next) => {
response.debugMiddlewares = [];
next();
response.on('finish', () => {
// Console log the debug middlewares
let message = `[${request.method}] ${request.originalUrl}\n`;
response.debugMiddlewares.forEach((m) => {
message += m.time
? `-> Middleware ${m.id} - ${m.time} ms\n`
: `-> Middleware ${m.id}\n`;
});
// Skip logging if the request is for static files
if (
request.currentRoute?.id === 'staticAsset' ||
request.currentRoute?.id === 'adminStaticAsset'
) {
return;
}
debug(message);
});
});
// Add public static middleware
app.use(publicStatic);
// Add theme public static middleware
app.use(themePublicStatic);
// Express session
const cookieSecret = getCookieSecret();
const sess = {
store:
process.env.NODE_ENV === 'test'
? undefined
: new (sessionStorage(session))({
pool
}),
secret: cookieSecret,
cookie: {
maxAge: getConfig('system.session.maxAge', 24 * 60 * 60 * 1000)
},
resave: getConfig('system.session.resave', false),
saveUninitialized: getConfig('system.session.saveUninitialized', true)
} as session.SessionOptions;
if (isProductionMode()) {
app.set('trust proxy', 1);
sess.cookie!.secure = false;
}
const adminSessionMiddleware = session({
...sess,
name: getAdminSessionCookieName()
});
const frontStoreSessionMiddleware = session({
...sess,
name: getFrontStoreSessionCookieName()
});
// Cookie parser
app.use(cookieParser(cookieSecret));
app.use((request, response, next) => {
const routes = getRoutes();
const method = request.method.toUpperCase();
const requestPath = request.originalUrl.split('?')[0];
const matchedRoutes = routes.filter((r) => {
const regexp = pathToRegexp(r.path, []);
const match = regexp.exec(requestPath);
if (match && r.method.includes(method)) {
return true;
} else {
return false;
}
});
if (matchedRoutes.length > 1) {
warning(
`Multiple routes matched for ${requestPath}. Please check your routes: ${matchedRoutes
.map((r) => r.id)
.join(', ')}. Route ${matchedRoutes[0].id} will be used.`
);
}
if (matchedRoutes.length) {
request.currentRoute = matchedRoutes[0];
next();
} else {
next();
}
});
const sessionMiddleware = (request, response, next) => {
const { currentRoute } = request;
if (currentRoute?.isApi) {
// We don't need session for api routes. Restful api should be stateless
next();
} else if (currentRoute?.isAdmin) {
adminSessionMiddleware(request, response, next);
} else {
frontStoreSessionMiddleware(request, response, next);
}
};
app.use(sessionMiddleware);
app.use(async (request, response, next) => {
// Get the request path, remove '/' from both ends
const path = request.originalUrl.split('?')[0].replace(/^\/|\/$/g, '');
// If the current route is already set, or the path contains .hot-update.json, .hot-update.js skip this middleware
if (request.currentRoute || path.includes('.hot-update')) {
return next();
}
// Also skip if we are running in the test mode
if (process.env.NODE_ENV === 'test') {
return next();
}
// Find the matched rewrite rule base on the request path
const rewriteRule = await select()
.from('url_rewrite')
.where('request_path', '=', `/${path}`)
.load(pool);
if (rewriteRule) {
// Find the route
const routes = getRoutes();
const route = routes.find((r) => {
const regexp = pathToRegexp(r.path);
const match = regexp.exec(rewriteRule.target_path);
if (match) {
request.locals = request.locals || {};
request.locals.customParams = {};
const keys: any[] = [];
pathToRegexp(r.path, keys);
keys.forEach((key, index) => {
request.locals.customParams[key.name] = match[index + 1];
});
return true;
}
return false;
});
// Get the current http method
const method = request.method.toUpperCase();
// Check if the route supports the current http method
if (route && route.method.includes(method)) {
request.currentRoute = route;
}
return next();
} else {
return next();
}
});
if (isDevelopmentMode()) {
// Admin webpack dev middleware - only for /backend/* paths
app.use((request, response, next) => {
if (request.path.startsWith('/backend/')) {
const adminDevMiddleware = getDevMiddleware(true);
adminDevMiddleware.waitUntilValid(() => {
const { stats } = adminDevMiddleware.context;
if (stats) {
response.locals.jsonWebpackStats = stats.toJson();
}
});
adminDevMiddleware(request, response, next);
} else {
next();
}
});
app.use((request, response, next) => {
if (request.path.startsWith('/__webpack_hmr_admin')) {
const adminHotMiddleware = getHotMiddleware(true);
adminHotMiddleware(request, response, next);
} else {
next();
}
});
// Frontstore webpack dev middleware - for all other paths
app.use((request, response, next) => {
if (
!request.path.startsWith('/backend/') &&
!request.path.startsWith('/__webpack_hmr_admin')
) {
const frontstoreDevMiddleware = getDevMiddleware(false);
frontstoreDevMiddleware.waitUntilValid(() => {
const { stats } = frontstoreDevMiddleware.context;
if (stats) {
response.locals.jsonWebpackStats = stats.toJson();
}
});
frontstoreDevMiddleware(request, response, next);
} else {
next();
}
});
app.use((request, response, next) => {
if (request.path.startsWith('/__webpack_hmr_frontstore')) {
const frontstoreHotMiddleware = getHotMiddleware(false);
frontstoreHotMiddleware(request, response, next);
} else {
next();
}
});
}
/** 404 Not Found handle */
app.use((request, response, next) => {
if (!request.currentRoute) {
response.status(404);
const routes = getRoutes();
request.currentRoute = routes.find((r) => r.id === 'notFound');
setPageMetaInfo(request, {
title: translate('Not found'),
description: translate('Not found')
});
next();
} else {
next();
}
});
}
================================================
FILE: packages/evershop/src/bin/lib/app.js
================================================
import express from 'express';
import { error } from '../../lib/log/logger.js';
import { Handler } from '../../lib/middleware/Handler.js';
import { getModuleMiddlewares } from '../../lib/middleware/index.js';
import { loadModuleRoutes } from '../../lib/router/loadModuleRoutes.js';
import { getRoutes } from '../../lib/router/Router.js';
import { getEnabledExtensions } from '../extension/index.js';
import { addDefaultMiddlewareFuncs } from './addDefaultMiddlewareFuncs.js';
import { getCoreModules } from './loadModules.js';
export const createApp = () => {
/** Create express app */
const app = express();
// Enable trust proxy
app.enable('trust proxy');
/* Loading modules and initilize routes, components and services */
const modules = getCoreModules();
// Load routes and middleware functions
modules.forEach((module) => {
try {
// Load middleware functions
getModuleMiddlewares(module.path);
// Load routes
loadModuleRoutes(module.path);
} catch (e) {
error(e);
process.exit(0);
}
});
/** Load extensions */
const extensions = getEnabledExtensions();
extensions.forEach((extension) => {
try {
// Load middleware functions
getModuleMiddlewares(extension.path);
// Load routes
loadModuleRoutes(extension.path);
} catch (e) {
error(e);
process.exit(0);
}
});
// Adding default middlewares
addDefaultMiddlewareFuncs(app);
const routes = getRoutes();
routes.forEach((route) => {
// app.all(route.path, Handler.middleware());
route.method.forEach((method) => {
switch (method.toUpperCase()) {
case 'GET':
app.get(route.path, Handler.middleware());
break;
case 'POST':
app.post(route.path, Handler.middleware());
break;
case 'PUT':
app.put(route.path, Handler.middleware());
break;
case 'DELETE':
app.delete(route.path, Handler.middleware());
break;
case 'PATCH':
app.patch(route.path, Handler.middleware());
break;
default:
app.get(route.path, Handler.middleware());
break;
}
});
});
app.use(Handler.middleware());
return app;
};
================================================
FILE: packages/evershop/src/bin/lib/bootstrap/bootstrap.ts
================================================
import { existsSync } from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
interface Module {
path: string;
}
export type BootstrapContext = {
command?: string;
env?: 'production' | 'development' | 'test';
process?: 'main' | 'cronjob' | 'event';
};
type BootstrapModule = {
default: (context: BootstrapContext) => Promise | void;
};
/**
* Loads and runs the bootstrap script from a module directory.
*/
export const loadBootstrapScript = async function loadBootstrapScript(
module: Module,
context: BootstrapContext = {}
): Promise {
const filePath = path.resolve(module.path, 'bootstrap.js');
if (!existsSync(filePath)) {
return;
}
// Convert path to a URL
const bootstrapPath = pathToFileURL(filePath).toString();
const bootstrap = (await import(bootstrapPath)) as BootstrapModule;
if (typeof bootstrap.default !== 'function') {
throw new Error(
'Bootstrap script must provide a default export as a function'
);
}
await bootstrap.default(context);
};
================================================
FILE: packages/evershop/src/bin/lib/bootstrap/migrate.js
================================================
import { existsSync, readdirSync } from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
import {
commit,
insertOnUpdate,
rollback,
select,
startTransaction
} from '@evershop/postgres-query-builder';
import semver from 'semver';
import { error } from '../../../lib/log/logger.js';
import { getConnection, pool } from '../../../lib/postgres/connection.js';
import { createMigrationTable } from '../../install/createMigrationTable.js';
async function getCurrentInstalledVersion(module, connection = null) {
/** Check for current installed version */
const check = await select()
.from('migration')
.where('module', '=', module)
.load(connection || pool);
if (!check) {
return '0.0.1';
} else {
return check.version;
}
}
async function migrateModule(module, connection = null) {
/** Check if the module has migration folder, if not ignore it */
if (!existsSync(path.resolve(module.path, 'migration'))) {
return;
}
const migrations = readdirSync(path.resolve(module.path, 'migration'), {
withFileTypes: true
})
.filter(
(dirent) =>
dirent.isFile() &&
dirent.name.match(/^Version-+([1-9].[0-9].[0-9])+.js$/)
)
.map((dirent) => dirent.name.replace('Version-', '').replace('.js', ''))
.sort((first, second) => semver.lt(first, second));
const currentInstalledVersion = await getCurrentInstalledVersion(
module.name,
connection
);
for (const version of migrations) {
/** If the version is lower or equal the installed version, ignore it */
if (semver.lte(version, currentInstalledVersion)) {
continue;
}
const migrationConnection = connection || (await getConnection());
if (!connection) {
await startTransaction(migrationConnection);
}
/** We expect the migration script to provide a function as a default export */
try {
const versionModule = await import(
pathToFileURL(
path.resolve(module.path, 'migration', `Version-${version}.js`)
)
);
await versionModule.default(migrationConnection);
await insertOnUpdate('migration', ['module'])
.given({
module: module.name,
version
})
.execute(migrationConnection, false);
if (!connection) {
await commit(migrationConnection);
}
} catch (e) {
if (!connection) {
await rollback(migrationConnection);
}
throw new Error(
`Migration failed for module ${module.name}, version ${version}\n${e}`
);
}
}
}
export async function migrate(modules, connection = null) {
try {
const psqlConnection = connection || (await getConnection());
// Create a migration table if not exists. This is for the first time installation
await createMigrationTable(psqlConnection);
for (const module of modules) {
await migrateModule(module, connection);
}
} catch (e) {
error(e);
process.exit(0);
}
}
================================================
FILE: packages/evershop/src/bin/lib/buildEntry.js
================================================
import fs from 'fs';
import { mkdir, writeFile } from 'fs/promises';
import path from 'path';
import { pathToFileURL } from 'url';
import { inspect } from 'util';
import JSON5 from 'json5';
import { getComponentsByRoute } from '../../lib/componee/getComponentsByRoute.js';
import { CONSTANTS } from '../../lib/helpers.js';
import { error } from '../../lib/log/logger.js';
import { generateComponentKey } from '../../lib/util/keyGenerator.js';
import { getRouteBuildPath } from '../../lib/webpack/getRouteBuildPath.js';
import { parseGraphql } from '../../lib/webpack/util/parseGraphql.js';
import { getEnabledWidgets } from '../../lib/widget/widgetManager.js';
/**
* Only pass the page routes, not api routes
*/
export async function buildEntry(routes, clientOnly = false) {
const widgets = getEnabledWidgets();
await Promise.all(
routes.map(async (route) => {
const imports = [];
const subPath = getRouteBuildPath(route);
const components = getComponentsByRoute(route);
if (!components) {
return;
}
/** Build layout and query */
const areas = {};
components.forEach((module) => {
if (!fs.existsSync(module)) {
return;
}
const source = fs.readFileSync(module, 'utf8');
// Regex matching 'export const layout = { ... }'
const layoutRegex =
/export\s+const\s+layout\s*=\s*{\s*areaId\s*:\s*['"]([^'"]+)['"],\s*sortOrder\s*:\s*(\d+)\s*,*\s*}/;
const match = source.match(layoutRegex);
if (match) {
// Remove everything before '{' from the beginning of the match
const check = match[0]
.replace(/^[^{]*/, '')
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2": ');
try {
const layout = JSON5.parse(check);
const id = generateComponentKey(module);
const url = pathToFileURL(module).toString();
imports.push(`import ${id} from '${url}';`);
areas[layout.areaId] = areas[layout.areaId] || {};
areas[layout.areaId][id] = {
id,
sortOrder: layout.sortOrder,
component: { default: `---${id}---` }
};
} catch (e) {
error(`Error parsing layout from ${module}`);
error(e);
}
}
});
let contentClient = `
import React from 'react';
import ReactDOM from 'react-dom';
import { Area } from '@evershop/evershop/components/common';
import {${
route.isAdmin ? 'HydrateAdmin' : 'HydrateFrontStore'
}} from '@evershop/evershop/components/common';
`;
areas['*'] = areas['*'] || {};
widgets.forEach((widget) => {
const url = route.isAdmin
? pathToFileURL(widget.settingComponent).toString()
: pathToFileURL(widget.component).toString();
const id = generateComponentKey(
route.isAdmin
? `admin_widget_${widget.type}`
: `widget_${widget.type}`
);
imports.push(`import ${id} from '${url}';`);
areas['*'][id] = {
id,
sortOrder: widget.sortOrder || 0,
component: {
default: `---${id}---`
}
};
});
contentClient += '\r\n';
contentClient += imports.join('\r\n');
contentClient += '\r\n';
contentClient += `Area.defaultProps.components = ${inspect(areas, {
depth: 5
})
.replace(/"---/g, '')
.replace(/---"/g, '')
.replace(/'---/g, '')
.replace(/---'/g, '')} `;
contentClient += '\r\n';
contentClient += `ReactDOM.hydrate(
${
route.isAdmin
? 'React.createElement(HydrateAdmin, null)'
: 'React.createElement(HydrateFrontStore, null)'
},
document.getElementById('app')
);`;
if (!fs.existsSync(path.resolve(subPath, 'client'))) {
await mkdir(path.resolve(subPath, 'client'), { recursive: true });
}
await writeFile(
path.resolve(subPath, 'client', 'entry.js'),
contentClient
);
if (!clientOnly) {
/** Build query */
const query = `${JSON.stringify(parseGraphql(components))}`;
// Loop through the widgets config and add the query to the widgets
let contentServer = `import React from 'react'; `;
contentServer += '\r\n';
contentServer += `import ReactDOM from 'react-dom'; `;
contentServer += '\r\n';
contentServer += `import { Area } from '@evershop/evershop/components/common';`;
contentServer += '\r\n';
contentServer += `import { renderHtml } from '@evershop/evershop/components/common';\r\n`;
contentServer += imports.join('\r\n');
contentServer += '\r\n';
contentServer += `export default renderHtml;\r\n`;
contentServer += `Area.defaultProps.components = ${inspect(areas, {
depth: 5
})
.replace(/"---/g, '')
.replace(/---"/g, '')
.replace(/'---/g, '')
.replace(/---'/g, '')} `;
if (!fs.existsSync(path.resolve(subPath, 'server'))) {
await mkdir(path.resolve(subPath, 'server'), { recursive: true });
}
await writeFile(
path.resolve(subPath, 'server', 'entry.js'),
contentServer
);
await writeFile(
path.resolve(subPath, 'server', 'query.graphql'),
query
);
}
})
);
}
================================================
FILE: packages/evershop/src/bin/lib/devEnvHelper.ts
================================================
import webpack from 'webpack';
import middleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import { createConfigClient } from '../../lib/webpack/dev/createConfigClient.js';
type DevConfig = {
admin: {
compiler?: webpack.Compiler;
devMiddleware?: ReturnType;
hotMiddleware?: ReturnType;
};
frontStore: {
compiler?: webpack.Compiler | null;
devMiddleware?: ReturnType | null;
hotMiddleware?: ReturnType | null;
};
};
const webpackConfig = {
admin: {},
frontStore: {}
} as DevConfig;
function getWebpackCompiler(isAdmin: boolean) {
const area = isAdmin ? 'admin' : 'frontStore';
if (!webpackConfig[area].compiler) {
webpackConfig[area].compiler = webpack(createConfigClient(isAdmin) as any);
}
return webpackConfig[area].compiler;
}
function getDevMiddleware(isAdmin: boolean) {
const area = isAdmin ? 'admin' : 'frontStore';
if (!webpackConfig[area].devMiddleware) {
const compiler = getWebpackCompiler(isAdmin);
const devMiddleware = middleware(compiler, {
serverSideRender: true,
publicPath: isAdmin ? '/backend/' : '/',
stats: 'none'
});
devMiddleware.context.logger.info = () => {};
webpackConfig[area].devMiddleware = devMiddleware;
}
return webpackConfig[area].devMiddleware;
}
function getHotMiddleware(isAdmin: boolean) {
const area = isAdmin ? 'admin' : 'frontStore';
if (!webpackConfig[area].hotMiddleware) {
const compiler = getWebpackCompiler(isAdmin);
const hotMiddleware = webpackHotMiddleware(compiler, {
path: isAdmin ? `/__webpack_hmr_admin` : `/__webpack_hmr_frontstore`
});
webpackConfig[area].hotMiddleware = hotMiddleware;
}
return webpackConfig[area].hotMiddleware;
}
export { getWebpackCompiler, getDevMiddleware, getHotMiddleware };
================================================
FILE: packages/evershop/src/bin/lib/loadModules.js
================================================
import { readdirSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const coreModules = [
{
name: 'auth',
resolve: path.resolve(__dirname, '../../modules/auth'),
path: path.resolve(__dirname, '../../modules/auth')
},
{
name: 'base',
resolve: path.resolve(__dirname, '../../modules/base'),
path: path.resolve(__dirname, '../../modules/base')
},
{
name: 'catalog',
resolve: path.resolve(__dirname, '../../modules/catalog'),
path: path.resolve(__dirname, '../../modules/catalog')
},
{
name: 'checkout',
resolve: path.resolve(__dirname, '../../modules/checkout'),
path: path.resolve(__dirname, '../../modules/checkout')
},
{
name: 'cms',
resolve: path.resolve(__dirname, '../../modules/cms'),
path: path.resolve(__dirname, '../../modules/cms')
},
{
name: 'cod',
resolve: path.resolve(__dirname, '../../modules/cod'),
path: path.resolve(__dirname, '../../modules/cod')
},
{
name: 'customer',
resolve: path.resolve(__dirname, '../../modules/customer'),
path: path.resolve(__dirname, '../../modules/customer')
},
{
name: 'graphql',
resolve: path.resolve(__dirname, '../../modules/graphql'),
path: path.resolve(__dirname, '../../modules/graphql')
},
{
name: 'oms',
resolve: path.resolve(__dirname, '../../modules/oms'),
path: path.resolve(__dirname, '../../modules/oms')
},
{
name: 'paypal',
resolve: path.resolve(__dirname, '../../modules/paypal'),
path: path.resolve(__dirname, '../../modules/paypal')
},
{
name: 'promotion',
resolve: path.resolve(__dirname, '../../modules/promotion'),
path: path.resolve(__dirname, '../../modules/promotion')
},
{
name: 'setting',
resolve: path.resolve(__dirname, '../../modules/setting'),
path: path.resolve(__dirname, '../../modules/setting')
},
{
name: 'stripe',
resolve: path.resolve(__dirname, '../../modules/stripe'),
path: path.resolve(__dirname, '../../modules/stripe')
},
{
name: 'tax',
resolve: path.resolve(__dirname, '../../modules/tax'),
path: path.resolve(__dirname, '../../modules/tax')
}
];
export function loadModule(path) {
return readdirSync(path, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => ({
name: dirent.name,
path: path.resolve(path, dirent.name)
}));
}
export function getCoreModules() {
return coreModules;
}
================================================
FILE: packages/evershop/src/bin/lib/normalizePort.js
================================================
/**
* Normalize a port into a number, string, or false.
*/
export function normalizePort() {
const port = parseInt(process.env.PORT, 10);
if (isNaN(port)) {
return 3000;
}
if (port >= 0) {
// port number
return port;
}
return 3000;
}
================================================
FILE: packages/evershop/src/bin/lib/onError.js
================================================
import { error } from '../../lib/log/logger.js';
import { normalizePort } from './normalizePort.js';
const port = normalizePort();
/**
* Event listener for HTTP server "err" event.
*/
export function onError(err) {
if (err.syscall !== 'listen') {
throw err;
}
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
// handle specific listen errors with friendly messages
switch (err.code) {
case 'EACCES':
error(`${bind} requires elevated privileges\n`);
process.exit(1);
break;
case 'EADDRINUSE':
error(`${bind} is already in use\n`);
process.exit(1);
break;
default:
throw err;
}
}
================================================
FILE: packages/evershop/src/bin/lib/onListening.js
================================================
import boxen from 'boxen';
import kleur from 'kleur';
import { normalizePort } from './normalizePort.js';
const port = normalizePort();
/**
* Event listener for HTTP server "listening" event.
*/
export function onListening() {
const message = boxen(
`Your website is running at "http://localhost:${port}"`,
{
title: 'EverShop',
titleAlignment: 'center',
padding: 1,
margin: 1,
borderColor: 'green'
}
);
// eslint-disable-next-line no-console
console.log(kleur.green(message));
}
================================================
FILE: packages/evershop/src/bin/lib/prepare.js
================================================
import { getAdminRoutes } from '../../src/lib/router/Router.js';
export function prepare(app, middlewares, routes) {
const adminRoutes = getAdminRoutes();
middlewares.forEach((m) => {
if (m.routeId === null) {
app.use(m.middleware);
} else if (m.routeId === 'admin') {
adminRoutes.forEach((route) => {
if (route.id !== 'adminStaticAsset' || m.id === 'isAdmin') {
route.method.forEach((method) => {
switch (method.toUpperCase()) {
case 'GET':
app.get(route.path, m.middleware);
break;
case 'POST':
app.post(route.path, m.middleware);
break;
case 'PUT':
app.put(route.path, m.middleware);
break;
case 'DELETE':
app.delete(route.path, m.middleware);
break;
default:
app.get(route.path, m.middleware);
break;
}
});
}
});
} else if (m.routeId === 'frontStore') {
app.all('*', (request, response, next) => {
const route = request.currentRoute;
if (route.isAdmin === true || route.id === 'staticAsset') {
return next();
}
return m.middleware(request, response, next);
});
} else {
const route = routes.find((r) => r.id === m.routeId);
if (route !== undefined) {
route.method.forEach((method) => {
switch (method.toUpperCase()) {
case 'GET':
app.get(route.path, m.middleware);
break;
case 'POST':
app.post(route.path, m.middleware);
break;
case 'PUT':
app.put(route.path, m.middleware);
break;
case 'DELETE':
app.delete(route.path, m.middleware);
break;
default:
app.get(route.path, m.middleware);
break;
}
});
}
}
});
}
================================================
FILE: packages/evershop/src/bin/lib/startCronProcess.ts
================================================
import path from 'path';
import { fileURLToPath } from 'url';
import spawn from 'cross-spawn';
import { error } from '../../lib/log/logger.js';
import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export function startCronProcess(context) {
// Spawn the child process to manage scheduled jobs
const jobArgs = [path.resolve(__dirname, '../../lib/cronjob/cronjob.js')];
if (isDevelopmentMode() || process.argv.includes('--debug')) {
jobArgs.push('--debug');
}
const jobChild = spawn('node', jobArgs, {
stdio: 'inherit',
env: {
...process.env,
bootstrapContext: JSON.stringify(context),
ALLOW_CONFIG_MUTATIONS: true
}
});
jobChild.on('error', (err) => {
error(`Error spawning job processor: ${err}`);
});
jobChild.unref();
return jobChild;
}
================================================
FILE: packages/evershop/src/bin/lib/startSubscriberProcess.ts
================================================
import path from 'path';
import { fileURLToPath } from 'url';
import spawn from 'cross-spawn';
import { error } from '../../lib/log/logger.js';
import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export function startSubscriberProcess(context) {
const args = [path.resolve(__dirname, '../../lib/event/event-manager.js')];
if (isDevelopmentMode() || process.argv.includes('--debug')) {
args.push('--debug');
}
const child = spawn('node', args, {
stdio: 'inherit',
env: {
...process.env,
bootstrapContext: JSON.stringify(context),
ALLOW_CONFIG_MUTATIONS: true
}
});
child.on('error', (err) => {
error(`Error spawning event processor: ${err}`);
});
child.unref();
return child;
}
================================================
FILE: packages/evershop/src/bin/lib/startUp.js
================================================
import http from 'http';
import path from 'path';
import { fileURLToPath } from 'url';
import config from 'config';
import spawn from 'cross-spawn';
import { error, debug } from '../../lib/log/logger.js';
import { Handler } from '../../lib/middleware/Handler.js';
import { lockHooks } from '../../lib/util/hookable.js';
import isDevelopmentMode from '../../lib/util/isDevelopmentMode.js';
import { lockRegistry } from '../../lib/util/registry.js';
import { validateConfiguration } from '../../lib/util/validateConfiguration.js';
import { getEnabledExtensions } from '../extension/index.js';
import { createApp } from './app.js';
import { loadBootstrapScript } from './bootstrap/bootstrap.js';
import { migrate } from './bootstrap/migrate.js';
import { getCoreModules } from './loadModules.js';
import { normalizePort } from './normalizePort.js';
import { onError } from './onError.js';
import { onListening } from './onListening.js';
import { startCronProcess } from './startCronProcess.js';
import { startSubscriberProcess } from './startSubscriberProcess.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const start = async function start(context, cb) {
const app = createApp();
/** Create a http server */
const server = http.createServer(app);
const modules = [...getCoreModules(), ...getEnabledExtensions()];
/** Loading bootstrap script from modules */
try {
for (const module of modules) {
await loadBootstrapScript(module, context);
}
lockHooks();
lockRegistry();
// Get the configuration (nodeconfig)
validateConfiguration(config);
} catch (e) {
error(e);
process.exit(0);
}
process.env.ALLOW_CONFIG_MUTATIONS = false;
/** Migration */
try {
await migrate(modules);
} catch (e) {
error(e);
process.exit(0);
}
/**
* Get port from environment and store in Express.
*/
const port = normalizePort();
app.set('port', port);
/** Start listening */
server.on('listening', () => {
onListening();
if (cb) {
cb();
}
});
server.on('error', onError);
server.listen(port);
// Spawn the child process to manage events
let subscriberChild = startSubscriberProcess(context);
let jobChild = startCronProcess(context);
process.on('exit', (code) => {
// Cleanup child processes on exit
if (subscriberChild && subscriberChild.pid) {
subscriberChild.kill('SIGTERM');
}
if (jobChild && jobChild.pid) {
jobChild.kill('SIGTERM');
}
if (code === 100) {
debug('Restarting the sever');
process.send('RESTART_ME');
}
});
process.on('RESTART_CRONJOB', () => {
debug('Restarting the cron job process');
jobChild.kill('SIGTERM');
jobChild = startCronProcess(context);
});
process.on('RESTART_SUBSCRIBER', () => {
debug('Restarting the subscriber process');
subscriberChild.kill('SIGTERM');
subscriberChild = startSubscriberProcess(context);
});
};
================================================
FILE: packages/evershop/src/bin/lib/watch/broadcast.js
================================================
import { getRoutes } from '../../../lib/router/Router.js';
export const broadcast = async () => {
const routes = getRoutes();
routes.forEach((route) => {
if (route.hotMiddleware) {
const { hotMiddleware } = route;
hotMiddleware.publish({
action: 'serverReloaded'
});
}
});
};
================================================
FILE: packages/evershop/src/bin/lib/watch/compileSwc.ts
================================================
import fs, { promises as fsp } from 'fs';
import type { PathLike } from 'fs';
import path from 'path';
import { execa } from 'execa';
import { CONSTANTS } from '../../../lib/helpers.js';
import { error, warning } from '../../../lib/log/logger.js';
export async function compileSwc(
srcPath: PathLike,
distPath: PathLike
): Promise {
// Check if the source is a file or directory
if (!fs.existsSync(srcPath)) {
warning(`Source path ${srcPath} does not exist.`);
return;
// Check if file extension is not either ts, js, tsx, or jsx
} else if (
fs.statSync(srcPath).isFile() &&
!['.ts', '.js', '.tsx', '.jsx'].includes(path.extname(srcPath as string))
) {
// For this case, we just force copy the file to the dist directory
try {
const directory = path.dirname(distPath as string);
await fsp.mkdir(directory, { recursive: true });
await fsp.copyFile(srcPath as string, distPath as string);
} catch (err) {
error(`Error copying ${srcPath} to ${distPath}:`);
throw err;
}
} else {
let cliOptions;
const configFile = path.resolve(CONSTANTS.LIBPATH, '../../.swcrc');
if (fs.statSync(srcPath).isDirectory()) {
cliOptions = [
srcPath as string,
'-d',
distPath as string,
'--config-file',
configFile,
'--strip-leading-paths',
'--copy-files'
];
} else {
cliOptions = [
srcPath as string,
'-o',
distPath as string,
'--config-file',
configFile,
'--strip-leading-paths'
];
}
try {
// Delete the dist directory if it exists using rimraf
await fsp.rm(distPath as string, { recursive: true, force: true });
await execa('swc', cliOptions, {
cwd: path.resolve(srcPath as string, '..')
});
} catch (err) {
error(`Error compiling ${srcPath}:`);
throw err;
}
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/effect.ts
================================================
import { existsSync } from 'fs';
import { basename, dirname } from 'path';
import { Application } from 'express';
import { minimatch } from 'minimatch';
import { has } from '../../../bin/dev/register.js';
import { getEnabledJobs } from '../../../lib/cronjob/jobManager.js';
import { debug, error } from '../../../lib/log/logger.js';
import { getRoute } from '../../../lib/router/Router.js';
import { broadcast } from './broadcast.js';
import { isRestartRequired } from './isRestartRequired.js';
import { isSrc } from './isSrc.js';
import { processors } from './processors/index.js';
import { Event } from './watchHandler.js';
export type Effect =
| 'restart'
| 'restart_cronjob'
| 'restart_event'
| 'add_middleware'
| 'remove_middleware'
| 'update_middleware'
| 'add_component'
| 'remove_component'
| 'update_component'
| 'add_api_route'
| 'remove_api_route'
| 'update_api_route'
| 'add_admin_route'
| 'remove_admin_route'
| 'update_admin_route'
| 'add_front_store_route'
| 'remove_front_store_route'
| 'update_front_store_route'
| 'update_graphql'
| 'unknown';
function isValidRouteFolder(name: string): boolean {
const segments = name.split('+');
// Make sure all segment match this regex: /^[a-zA-Z]+$/
return segments.every((segment) => /^[a-zA-Z]+$/.test(segment));
}
export function detectEffect(event: Event): Effect {
const jobs = getEnabledJobs();
if (isRestartRequired(event)) {
return 'restart'; // No specific effect, just a restart required
} else if (minimatch(event.path.toString(), '**/*/[A-Z]*.+(jsx|tsx)')) {
const routeFolder = basename(dirname(event.path.toString()));
if (!isValidRouteFolder(routeFolder)) {
return 'unknown'; // Not a valid route folder, skip
}
if (event.type === 'create') {
if (
minimatch(
event.path.toString(),
'**/pages/+(admin|frontStore)/[A-Z]*.+(jsx|tsx)'
)
) {
return 'update_component';
} else {
return 'add_component';
}
} else if (event.type === 'delete') {
return 'remove_component';
} else {
return 'update_component';
}
} else if (minimatch(event.path.toString(), '**/+(api|admin|frontStore)/*')) {
const fileName = basename(event.path.toString());
if (!isValidRouteFolder(fileName)) {
return 'unknown'; // Not a valid route folder, skip
}
if (event.type === 'delete') {
const route = getRoute(fileName);
if (route) {
const routePath = route.path;
if (!existsSync(routePath)) {
// If the route file does not exist, it means the route folder is deleted. We can safely delete the route.
if (route.isApi) {
return 'remove_api_route';
} else if (route.isAdmin) {
return 'remove_admin_route';
} else {
return 'remove_front_store_route';
}
} else {
return 'remove_middleware'; // The route folder still exists, so we just need to remove the middleware
}
} else {
// This folder is not representing a route, so we just need to take care of midldleware functions
return 'remove_middleware';
}
} else {
return 'unknown';
}
} else if (
minimatch(
event.path.toString(),
'**/+(api|admin|frontStore)/*/[a-z[]*.+(js|ts)'
)
) {
const routeFolder = basename(dirname(event.path.toString()));
if (!isValidRouteFolder(routeFolder)) {
return 'unknown'; // Not a valid route folder, skip
}
if (event.type === 'create') {
return 'add_middleware'; // This is a middleware file
} else if (event.type === 'delete') {
return 'remove_middleware';
} else {
return 'update_middleware';
}
} else if (minimatch(event.path.toString(), '**/api/*/route.json')) {
const routeFolder = basename(dirname(event.path.toString()));
if (!isValidRouteFolder(routeFolder)) {
return 'unknown'; // Not a valid route folder, skip
}
if (event.type === 'create') {
return 'add_api_route';
} else if (event.type === 'delete') {
return 'remove_api_route';
} else {
return 'update_api_route';
}
} else if (minimatch(event.path.toString(), '**/api/*/payloadSchema.json')) {
// This is a payload schema file for an API route
const routeFolder = basename(dirname(event.path.toString()));
if (!isValidRouteFolder(routeFolder)) {
return 'unknown'; // Not a valid route folder, skip
}
return 'update_api_route';
} else if (minimatch(event.path.toString(), '**/pages/admin/*/route.json')) {
const routeFolder = basename(dirname(event.path.toString()));
if (!isValidRouteFolder(routeFolder)) {
return 'unknown'; // Not a valid route folder, skip
}
if (event.type === 'create') {
return 'add_admin_route';
} else if (event.type === 'delete') {
return 'remove_admin_route';
} else {
return 'update_admin_route';
}
} else if (
minimatch(event.path.toString(), '**/pages/frontStore/*/route.json')
) {
const routeFolder = basename(dirname(event.path.toString()));
if (!isValidRouteFolder(routeFolder)) {
return 'unknown'; // Not a valid route folder, skip
}
if (event.type === 'create') {
return 'add_front_store_route';
} else if (event.type === 'delete') {
return 'remove_front_store_route';
} else {
return 'update_front_store_route';
}
} else if (
minimatch(event.path.toString(), '**/*/*.graphql') ||
minimatch(event.path.toString(), '**/*/*.resolvers.+(ts|js)')
) {
return 'update_graphql'; // GraphQL schema or resolvers file
} else if (minimatch(event.path.toString(), '**/subscribers/**/*.+(ts|js)')) {
return 'restart_event';
}
// Check if the file is a job file
else if (
event.path &&
jobs.some(
(job) =>
job.resolve ===
event.path.toString().replace('src', 'dist').replace(/\.ts$/, '.js')
)
) {
return 'restart_cronjob';
} else if (isSrc(event.path.toString())) {
const distPath = event.path
.toString()
.replace('src', 'dist')
.replace(/\.ts$/, '.js');
if (has(distPath)) {
// This module is being used in the application, so we need to restart the process
return 'restart';
} else {
return 'unknown'; // This is a source file, but not used in the application
}
} else {
const distPath = event.path.toString();
if (has(distPath)) {
// This module is being used in the application, so we need to restart the process
return 'restart';
} else {
return 'unknown'; // This is a source file, but not used in the application
}
}
}
export function applyEffects(events: Event[], app: Application) {
for (const event of events) {
if (!event.effect) {
continue; // Skip if no effect is detected
} else {
const processor = processors[event.effect];
if (processor) {
try {
debug(`Applying changes: ${event.effect} for ${event.path}`);
processor(app, event);
} catch (e) {
error(`Error applying changes for ${event.path}:`);
error(e);
}
} else {
debug(`No processor found for effect type: ${event.effect}`);
}
}
}
// Call broadcast to notify all clients about the changes if there are any known effects
if (
events.some(
(e) =>
e.effect &&
!['unknown', 'restart_cronjob', 'restart_event'].includes(e.effect) &&
!e.effect.includes('component')
)
) {
debug('Broadcasting changes to all clients');
broadcast();
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/getDistPaths.ts
================================================
import { PathLike } from 'fs';
export function getDistPaths(): PathLike[] {
return ['dist', 'packages/evershop/dist', 'packages/agegate/dist'];
}
================================================
FILE: packages/evershop/src/bin/lib/watch/getRootPaths.ts
================================================
import { PathLike } from 'fs';
import path from 'path';
import type { Event } from './watchHandler.js';
/**
* Deduplicates a list of paths, keeping only the top-most created folders.
* @param {Array<{ path: string, type: string }>} entries
* @returns {Event[]} Top-level unique root folders
*/
export function getRootPaths(entries: Event[]): Event[] {
const sortedPaths = entries
.map((entry) => path.resolve(entry.path as string))
.sort();
const roots: Event[] = [];
for (const current of sortedPaths) {
if (!roots.some((root) => current.startsWith(root.path + path.sep))) {
roots.push({
path: current,
type: entries.find((entry) => entry.path === current)?.type || 'create'
});
}
}
return roots;
}
================================================
FILE: packages/evershop/src/bin/lib/watch/getSrcPaths.ts
================================================
import { PathLike } from 'fs';
import path from 'path';
import { getEnabledExtensions } from '../../../bin/extension/index.js';
import { CONSTANTS } from '../../../lib/helpers.js';
import { getEnabledTheme } from '../../../lib/util/getEnabledTheme.js';
export function getSrcPaths(): PathLike[] {
const extensions = getEnabledExtensions();
const theme = getEnabledTheme();
return extensions
.filter((ext) => ext.srcPath)
.map((ext) => ext.srcPath as PathLike)
.concat(
!CONSTANTS.MODULESPATH.includes('node_modules')
? (path.resolve(
CONSTANTS.ROOTPATH,
'packages/evershop/src/'
) as PathLike)
: []
)
.concat(theme?.srcPath ? (theme.srcPath as PathLike) : []);
}
================================================
FILE: packages/evershop/src/bin/lib/watch/isDist.js
================================================
import path from 'path';
import { getDistPaths } from './getDistPaths.js';
export function isDist(pathName) {
if (
getDistPaths().some((distPath) => pathName.startsWith(distPath + path.sep))
) {
return true;
} else {
return false;
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/isRestartRequired.ts
================================================
import path from 'path';
import { CONSTANTS } from '../../../lib/helpers.js';
import { isSrc } from './isSrc.js';
import { Event } from './watchHandler.js';
export function isRestartRequired(event: Event) {
if (isSrc(event.path)) {
return false;
} else if (event.path === path.resolve(CONSTANTS.ROOTPATH, '.env')) {
// If the .env file is changed, we need to restart the server
return true;
} else {
const configPath = path.resolve(CONSTANTS.ROOTPATH, 'config');
if (
event.path.toString().startsWith(configPath) &&
path.extname(event.path as string) === '.json'
) {
// If a config JSON file is changed, we need to restart the server
return true;
}
return false;
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/isSrc.js
================================================
import path from 'path';
import { getSrcPaths } from './getSrcPaths.js';
export function isSrc(pathName) {
if (
getSrcPaths().some((srcPath) => pathName.startsWith(srcPath + path.sep))
) {
return true;
} else {
return false;
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/addAdminRoute.ts
================================================
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { addRoute, hasRoute } from '../../../../lib/router/Router.js';
import { parseRoute } from '../../../../lib/router/scanForRoutes.js';
import { Event } from '../watchHandler.js';
export function addAdminRoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const route = parseRoute(jsonPath, true, false);
if (hasRoute(route?.id)) {
warning(`Route ${route?.id} already exists. Skipping adding new route.`);
} else {
addRoute(route);
}
} catch (error) {
warning(
`Failed to add new route from ${event.path}: ${error.message}. Skipping.`
);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/addApiRoute.ts
================================================
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { Handler } from '../../../../lib/middleware/Handler.js';
import { addRoute, hasRoute } from '../../../../lib/router/Router.js';
import { parseRoute } from '../../../../lib/router/scanForRoutes.js';
import { Event } from '../watchHandler.js';
export function addApiRoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const route = parseRoute(jsonPath, false, true);
if (!route || hasRoute(route?.id)) {
warning(`Route ${route?.id} already exists. Skipping adding new route.`);
} else {
addRoute(route);
for (const method of route.methods) {
switch (method.toUpperCase()) {
case 'GET':
app.get(route.path, Handler.middleware());
break;
case 'POST':
app.post(route.path, Handler.middleware());
break;
case 'PUT':
app.put(route.path, Handler.middleware());
break;
case 'DELETE':
app.delete(route.path, Handler.middleware());
break;
case 'PATCH':
app.patch(route.path, Handler.middleware());
break;
default:
app.get(route.path, Handler.middleware());
break;
}
}
}
} catch (error) {
warning(
`Failed to add new route from ${event.path}: ${error.message}. Skipping.`
);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/addComponent.ts
================================================
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { Event } from '../watchHandler.js';
export function addComponent(app: Application, event: Event) {
// Do nothing. Let ThemeWatcherPlugin handle this.
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/addFrontStoreRoute.ts
================================================
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { addRoute, hasRoute } from '../../../../lib/router/Router.js';
import { parseRoute } from '../../../../lib/router/scanForRoutes.js';
import { Event } from '../watchHandler.js';
export function addFrontStoreRoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const route = parseRoute(jsonPath, false, false);
if (hasRoute(route?.id)) {
warning(`Route ${route?.id} already exists. Skipping adding new route.`);
} else {
addRoute(route);
}
} catch (error) {
warning(
`Failed to add new route from ${event.path}: ${error.message}. Skipping.`
);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/addMiddleware.ts
================================================
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { Handler } from '../../../../lib/middleware/Handler.js';
import { Event } from '../watchHandler.js';
export function addMiddleware(app: Application, event: Event) {
try {
const filePath = event.jsPath?.toString();
Handler.addMiddlewareFromPath(filePath);
} catch (error) {
warning(
`Failed to add new middleware from ${event.jsPath}: ${error.message}. Skipping.`
);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/deleteARoute.ts
================================================
import { basename, dirname } from 'path';
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { deleteRoute, hasRoute } from '../../../../lib/router/Router.js';
import { Event } from '../watchHandler.js';
export function deleteARoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const routeId = jsonPath.includes('route.json')
? basename(dirname(jsonPath))
: basename(jsonPath);
if (hasRoute(routeId)) {
deleteRoute(routeId);
}
} catch (error) {
warning(
`Failed to delete route from ${event.path}: ${error.message}. Skipping.`
);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/index.ts
================================================
import { Application } from 'express';
import { Effect } from '../effect.js';
import { addAdminRoute } from './addAdminRoute.js';
import { addApiRoute } from './addApiRoute.js';
import { addComponent } from './addComponent.js';
import { addFrontStoreRoute } from './addFrontStoreRoute.js';
import { addMiddleware } from './addMiddleware.js';
import { deleteARoute } from './deleteARoute.js';
import { removeMiddleware } from './removeMiddleware.js';
import { restartCronJob } from './restartCronJob.js';
import { restartSubscriber } from './restartSubscriber.js';
import { updateAdminRoute } from './updateAdminRoute.js';
import { updateApiRoute } from './updateApiRoute.js';
import { updateFrontStoreRoute } from './updateFrontStoreRoute.js';
export type Processor = {
[key in Effect]?: (app: Application, event: any) => void;
};
export const processors: Processor = {
add_api_route: addApiRoute,
update_api_route: updateApiRoute,
add_front_store_route: addFrontStoreRoute,
update_front_store_route: updateFrontStoreRoute,
add_admin_route: addAdminRoute,
update_admin_route: updateAdminRoute,
remove_api_route: deleteARoute,
remove_admin_route: deleteARoute,
remove_front_store_route: deleteARoute,
add_middleware: addMiddleware,
remove_middleware: removeMiddleware,
update_middleware: () => {},
update_component: () => {
// No operation for update_component, as it is handled by the compiler}
},
remove_component: () => {
// No operation for update_component, as it is handled by the compiler}
},
add_component: addComponent,
update_graphql: () => {},
restart_cronjob: () => {
restartCronJob();
},
restart_event: () => {
restartSubscriber();
}
};
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/removeMiddleware.ts
================================================
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { Handler } from '../../../../lib/middleware/Handler.js';
import { Event } from '../watchHandler.js';
export function removeMiddleware(app: Application, event: Event) {
try {
const filePath = event.jsPath?.toString();
Handler.removeMiddlewares(filePath);
} catch (error) {
warning(
`Failed to remove middleware from ${event.jsPath}: ${error.message}. Skipping.`
);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/restart.ts
================================================
export function restartProcess() {
process.exit(100);
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/restartCronJob.ts
================================================
export function restartCronJob() {
(process as NodeJS.EventEmitter).emit('RESTART_CRONJOB');
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/restartSubscriber.ts
================================================
export function restartSubscriber() {
(process as NodeJS.EventEmitter).emit('RESTART_SUBSCRIBER');
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/touch.js
================================================
import { resolve } from 'path';
import touch from 'touch';
import { CONSTANTS } from '../../../../lib/helpers.js';
export function justATouch(path) {
touch(
path ||
resolve(
CONSTANTS.MODULESPATH,
'../components/common/react/client/Index.js'
)
);
}
export async function touchList(paths) {
await Promise.all(paths.map((p) => touch(p)));
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/updateAdminRoute.ts
================================================
import { dirname, join } from 'path';
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { addRoute } from '../../../../lib/router/Router.js';
import { parseRoute } from '../../../../lib/router/scanForRoutes.js';
import { Event } from '../watchHandler.js';
export function updateAdminRoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const route = parseRoute(
join(dirname(jsonPath), 'route.json'),
true,
false
);
addRoute(route);
} catch (error) {
warning(`Failed to update route from ${event.path}: ${error.message}`);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/updateApiRoute.ts
================================================
import { dirname, join } from 'path';
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { addRoute } from '../../../../lib/router/Router.js';
import { parseRoute } from '../../../../lib/router/scanForRoutes.js';
import { Event } from '../watchHandler.js';
export function updateApiRoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const route = parseRoute(
join(dirname(jsonPath), 'route.json'),
false,
true
);
addRoute(route);
} catch (error) {
warning(`Failed to update route from ${event.path}: ${error.message}`);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/processors/updateFrontStoreRoute.ts
================================================
import { dirname, join } from 'path';
import { Application } from 'express';
import { warning } from '../../../../lib/log/logger.js';
import { addRoute } from '../../../../lib/router/Router.js';
import { parseRoute } from '../../../../lib/router/scanForRoutes.js';
import { Event } from '../watchHandler.js';
export function updateFrontStoreRoute(app: Application, event: Event) {
try {
const jsonPath = event.path.toString();
const route = parseRoute(
join(dirname(jsonPath), 'route.json'),
false,
false
);
addRoute(route);
} catch (error) {
warning(`Failed to update route from ${event.path}: ${error.message}`);
}
}
================================================
FILE: packages/evershop/src/bin/lib/watch/watchHandler.ts
================================================
import { PathLike, readdirSync, rmSync, statSync } from 'fs';
import path from 'path';
import { Application } from 'express';
import { error } from '../../../lib/log/logger.js';
import { compileSwc } from './compileSwc.js';
import { applyEffects, detectEffect, Effect } from './effect.js';
import { isDist } from './isDist.js';
import { isSrc } from './isSrc.js';
import { restartProcess } from './processors/restart.js';
export type Event = {
path: PathLike;
type: 'create' | 'update' | 'delete';
jsPath?: PathLike;
effect?: Effect;
};
export async function watchHandler(events: Event[], app: Application) {
if (
events.length === 2 &&
events.some((e) => e.type === 'delete') &&
events.some((e) => e.type === 'create')
) {
// Likely a rename
// Sort the event make sure the delete comes first
events.sort((a, b) => (a.type === 'delete' ? -1 : 1));
// Travel the create event and if this is a folder, we need to add create event for every sub-file
for (const event of events) {
if (event.type === 'create') {
// Check if the path is a directory
try {
const stats = statSync(event.path);
if (stats.isDirectory()) {
// If it's a directory, we need to add create events for every file in the directory
const files = readdirSync(event.path);
for (const file of files) {
const filePath = path.resolve(event.path as string, file);
events.push({
path: filePath,
type: 'create'
});
}
}
} catch (e) {
error(`Error reading directory ${event.path}:`);
error(e);
}
}
}
}
// Handle the watch event
for (const event of events) {
event.effect = detectEffect(event);
if (event.effect === 'restart') {
restartProcess();
break; // Exit the loop if a restart is required, no need to process further
}
if (isDist(event.path)) {
continue;
}
if (isSrc(event.path)) {
const distPath = event.path
.toString()
.replace('src', 'dist')
.replace(/\.ts$/, '.js')
.replace(/\.tsx$/, '.js')
.replace(/\.jsx$/, '.js'); // Ensure the path ends with .js
event.jsPath = distPath; // Set the compiled JS path
if (event.type === 'delete') {
// Delete whatever is necessary in the dist folder
rmSync(distPath as string, { recursive: true, force: true });
} else {
// Run swc to compile the files
// Get the dist path from the event by replacing the first 'src' with 'dist' and ts to js if this is a ts file
await compileSwc(event.path, distPath);
}
}
}
applyEffects(events, app);
}
================================================
FILE: packages/evershop/src/bin/seed/data/attributes.json
================================================
[
{
"attribute_code": "color",
"attribute_name": "Color",
"type": "select",
"is_required": 1,
"display_on_frontend": 1,
"sort_order": 10,
"is_filterable": 1,
"groups": [],
"options": [
{ "option_text": "Black" },
{ "option_text": "White" },
{ "option_text": "Red" },
{ "option_text": "Blue" },
{ "option_text": "Green" },
{ "option_text": "Yellow" },
{ "option_text": "Pink" },
{ "option_text": "Gray" },
{ "option_text": "Navy" },
{ "option_text": "Beige" }
]
},
{
"attribute_code": "size",
"attribute_name": "Size",
"type": "select",
"is_required": 1,
"display_on_frontend": 1,
"sort_order": 20,
"is_filterable": 1,
"groups": [],
"options": [
{ "option_text": "XS" },
{ "option_text": "S" },
{ "option_text": "M" },
{ "option_text": "L" },
{ "option_text": "XL" },
{ "option_text": "XXL" }
]
}
]
================================================
FILE: packages/evershop/src/bin/seed/data/categories.json
================================================
[
{
"name": "Accessories",
"url_key": "accessories",
"description": [
{
"id": "r__accessories",
"columns": [
{
"size": 1,
"id": "c__accessories",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "acc_block_1",
"type": "paragraph",
"data": {
"text": "Complete your look with our stylish accessories"
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"meta_title": "Fashion Accessories",
"meta_description": "Browse our collection of fashion accessories",
"meta_keywords": "accessories, fashion accessories, style",
"status": 1,
"include_in_nav": 1
}
]
================================================
FILE: packages/evershop/src/bin/seed/data/collections.json
================================================
[
{
"name": "Featured Products",
"code": "homepage",
"description": [
{
"id": "r__featured",
"columns": [
{
"size": 1,
"id": "c__featured",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "fp_block_1",
"type": "paragraph",
"data": {
"text": "Featured products displayed on the homepage"
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
]
},
{
"name": "Summer Collection",
"code": "summer-2024",
"description": [
{
"id": "r__summer",
"columns": [
{
"size": 1,
"id": "c__summer",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "sc_block_1",
"type": "paragraph",
"data": {
"text": "Hot picks for the summer season"
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
]
},
{
"name": "Winter Essentials",
"code": "winter-essentials",
"description": [
{
"id": "r__winter",
"columns": [
{
"size": 1,
"id": "c__winter",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "we_block_1",
"type": "paragraph",
"data": {
"text": "Stay warm and stylish this winter"
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
]
},
{
"name": "Trending Now",
"code": "trending",
"description": [
{
"id": "r__trending",
"columns": [
{
"size": 1,
"id": "c__trending",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "tn_block_1",
"type": "paragraph",
"data": {
"text": "What's hot and trending right now"
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
]
}
]
================================================
FILE: packages/evershop/src/bin/seed/data/pages.json
================================================
[
{
"status": true,
"url_key": "about-us",
"name": "About Us",
"content": [
{
"id": "r__about_us",
"columns": [
{
"size": 1,
"id": "c__about_us",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "about_us_h2",
"type": "header",
"data": {
"text": "Welcome to Our Store",
"level": 2
}
},
{
"id": "about_us_p1",
"type": "paragraph",
"data": {
"text": "We are passionate about bringing you high-quality ceramic and stainless steel products that combine functionality with elegant design. Our carefully curated collection features items that enhance your daily life, from morning coffee to home organization."
}
},
{
"id": "about_us_h2",
"type": "header",
"data": {
"text": "Our Mission",
"level": 2
}
},
{
"id": "about_us_img1",
"type": "image",
"data": {
"file": {
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/banner-one.jpg",
"width": 2400,
"height": 1200
},
"caption": "Our carefully curated collection",
"withBorder": false,
"withBackground": false,
"stretched": false
}
},
{
"id": "about_us_p2",
"type": "paragraph",
"data": {
"text": "We believe that everyday objects should be both beautiful and practical. That's why we source products that are not only aesthetically pleasing but also durable and functional. Each item in our collection is selected with care to ensure it meets our high standards."
}
},
{
"id": "about_us_h3",
"type": "header",
"data": {
"text": "Quality You Can Trust",
"level": 2
}
},
{
"id": "about_us_p3",
"type": "paragraph",
"data": {
"text": "All our products are made from premium materials - from food-safe ceramics to BPA-free stainless steel. We work directly with manufacturers who share our commitment to quality and sustainability. Whether you're looking for office supplies, drinkware, or home decor, you can trust that every item has been thoughtfully designed and rigorously tested."
}
},
{
"id": "about_us_h4",
"type": "header",
"data": {
"text": "Customer Satisfaction",
"level": 2
}
},
{
"id": "about_us_p4",
"type": "paragraph",
"data": {
"text": "Your satisfaction is our top priority. We offer fast shipping, easy returns, and dedicated customer support to ensure your shopping experience is seamless. If you have any questions about our products or need assistance, our team is always here to help."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"meta_title": "About Us - Learn More About Our Store",
"meta_keywords": "about us, our story, company information",
"meta_description": "Learn more about our mission to bring you high-quality ceramic and stainless steel products that combine functionality with elegant design."
}
]
================================================
FILE: packages/evershop/src/bin/seed/data/products.json
================================================
[
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "CUP-001-WHT",
"name": "Ceramic Coffee Cup - White",
"url_key": "ceramic-coffee-cup-white",
"price": 15,
"weight": 300,
"meta_title": "Ceramic Coffee Cup - White",
"meta_description": "Modern ceramic coffee cup in white color, perfect for your morning coffee",
"meta_keywords": "coffee cup, ceramic cup, white cup, drinkware",
"description": [
{
"id": "r__cup_white",
"columns": [
{
"size": 1,
"id": "c__cup_white",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "cup_white_p1",
"type": "paragraph",
"data": {
"text": "Start your day right with our elegant Ceramic Coffee Cup. Crafted from high-quality ceramic, this cup features a smooth finish and comfortable grip."
}
},
{
"id": "cup_white_p2",
"type": "paragraph",
"data": {
"text": "The classic design makes it perfect for both home and office use. Holds 12oz of your favorite beverage and is both microwave and dishwasher safe."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 100,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "ceramic-coffee-cup",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/cup-white.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "White"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "CUP-001-BLK",
"name": "Ceramic Coffee Cup - Black",
"url_key": "ceramic-coffee-cup-black",
"price": 15,
"weight": 300,
"meta_title": "Ceramic Coffee Cup - Black",
"meta_description": "Modern ceramic coffee cup in black color, perfect for your morning coffee",
"meta_keywords": "coffee cup, ceramic cup, black cup, drinkware",
"description": [
{
"id": "r__cup_black",
"columns": [
{
"size": 1,
"id": "c__cup_black",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "cup_black_p1",
"type": "paragraph",
"data": {
"text": "Start your day right with our elegant Ceramic Coffee Cup. Crafted from high-quality ceramic, this cup features a smooth finish and comfortable grip."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 100,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "ceramic-coffee-cup",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/cup-black.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Black"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "CUP-001-YEL",
"name": "Ceramic Coffee Cup - Yellow",
"url_key": "ceramic-coffee-cup-yellow",
"price": 15,
"weight": 300,
"meta_title": "Ceramic Coffee Cup - Yellow",
"meta_description": "Modern ceramic coffee cup in yellow color, perfect for your morning coffee",
"meta_keywords": "coffee cup, ceramic cup, yellow cup, drinkware",
"description": [
{
"id": "r__cup_yellow",
"columns": [
{
"size": 1,
"id": "c__cup_yellow",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "cup_yellow_p1",
"type": "paragraph",
"data": {
"text": "Start your day right with our elegant Ceramic Coffee Cup. Crafted from high-quality ceramic, this cup features a smooth finish and comfortable grip."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 100,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "ceramic-coffee-cup",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/cup-yellow.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Yellow"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "PEN-002-WHT",
"name": "Desk Pen Holder - White",
"url_key": "desk-pen-holder-white",
"price": 12,
"weight": 200,
"meta_title": "Desk Pen Holder - White",
"meta_description": "Stylish desk pen holder to keep your workspace organized",
"meta_keywords": "pen holder, desk organizer, office supplies, white",
"description": [
{
"id": "r__pen_white",
"columns": [
{
"size": 1,
"id": "c__pen_white",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "pen_white_p1",
"type": "paragraph",
"data": {
"text": "Keep your desk tidy and organized with our modern Desk Pen Holder. Features multiple compartments for pens, pencils, scissors, and other office supplies."
}
},
{
"id": "pen_white_p2",
"type": "paragraph",
"data": {
"text": "Made from durable materials with a sleek finish that complements any workspace. Perfect for home office or corporate settings."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 150,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "new-arrivals"],
"category": "accessories",
"variant_group": "desk-pen-holder",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/pen-holder-white.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "White"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "PEN-002-BLK",
"name": "Desk Pen Holder - Black",
"url_key": "desk-pen-holder-black",
"price": 12,
"weight": 200,
"meta_title": "Desk Pen Holder - Black",
"meta_description": "Stylish desk pen holder to keep your workspace organized",
"meta_keywords": "pen holder, desk organizer, office supplies, black",
"description": [
{
"id": "r__pen_black",
"columns": [
{
"size": 1,
"id": "c__pen_black",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "pen_black_p1",
"type": "paragraph",
"data": {
"text": "Keep your desk tidy and organized with our modern Desk Pen Holder. Features multiple compartments for pens, pencils, scissors, and other office supplies."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 150,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "new-arrivals"],
"category": "accessories",
"variant_group": "desk-pen-holder",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/pen-holder-black.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Black"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "PEN-002-YEL",
"name": "Desk Pen Holder - Yellow",
"url_key": "desk-pen-holder-yellow",
"price": 12,
"weight": 200,
"meta_title": "Desk Pen Holder - Yellow",
"meta_description": "Stylish desk pen holder to keep your workspace organized",
"meta_keywords": "pen holder, desk organizer, office supplies, yellow",
"description": [
{
"id": "r__pen_yellow",
"columns": [
{
"size": 1,
"id": "c__pen_yellow",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "pen_yellow_p1",
"type": "paragraph",
"data": {
"text": "Keep your desk tidy and organized with our modern Desk Pen Holder. Features multiple compartments for pens, pencils, scissors, and other office supplies."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 150,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "new-arrivals"],
"category": "accessories",
"variant_group": "desk-pen-holder",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/pen-holder-yellow.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Yellow"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "BOWL-003-WHT",
"name": "Ceramic Candy Bowl - White",
"url_key": "ceramic-candy-bowl-white",
"price": 18,
"weight": 400,
"meta_title": "Ceramic Candy Bowl - White",
"meta_description": "Elegant ceramic bowl perfect for candy, snacks, or decorative use",
"meta_keywords": "candy bowl, ceramic bowl, serving bowl, white",
"description": [
{
"id": "r__bowl_white",
"columns": [
{
"size": 1,
"id": "c__bowl_white",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "bowl_white_p1",
"type": "paragraph",
"data": {
"text": "Add a touch of elegance to your table with our Ceramic Candy Bowl. Perfect for serving candy, nuts, or small snacks at parties and gatherings."
}
},
{
"id": "bowl_white_p2",
"type": "paragraph",
"data": {
"text": "The smooth ceramic finish and timeless design make it both functional and decorative. Also great for holding keys, jewelry, or other small items."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 80,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "ceramic-candy-bowl",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/bowl-white.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "White"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "BOWL-003-BLK",
"name": "Ceramic Candy Bowl - Black",
"url_key": "ceramic-candy-bowl-black",
"price": 18,
"weight": 400,
"meta_title": "Ceramic Candy Bowl - Black",
"meta_description": "Elegant ceramic bowl perfect for candy, snacks, or decorative use",
"meta_keywords": "candy bowl, ceramic bowl, serving bowl, black",
"description": [
{
"id": "r__bowl_black",
"columns": [
{
"size": 1,
"id": "c__bowl_black",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "bowl_black_p1",
"type": "paragraph",
"data": {
"text": "Add a touch of elegance to your table with our Ceramic Candy Bowl. Perfect for serving candy, nuts, or small snacks at parties and gatherings."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 80,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "ceramic-candy-bowl",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/bowl-black.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Black"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "BOWL-003-YEL",
"name": "Ceramic Candy Bowl - Yellow",
"url_key": "ceramic-candy-bowl-yellow",
"price": 18,
"weight": 400,
"meta_title": "Ceramic Candy Bowl - Yellow",
"meta_description": "Elegant ceramic bowl perfect for candy, snacks, or decorative use",
"meta_keywords": "candy bowl, ceramic bowl, serving bowl, yellow",
"description": [
{
"id": "r__bowl_yellow",
"columns": [
{
"size": 1,
"id": "c__bowl_yellow",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "bowl_yellow_p1",
"type": "paragraph",
"data": {
"text": "Add a touch of elegance to your table with our Ceramic Candy Bowl. Perfect for serving candy, nuts, or small snacks at parties and gatherings."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 80,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "ceramic-candy-bowl",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/bowl-yellow.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Yellow"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "VASE-004-WHT",
"name": "Modern Ceramic Vase - White",
"url_key": "modern-ceramic-vase-white",
"price": 25,
"weight": 500,
"meta_title": "Modern Ceramic Vase - White",
"meta_description": "Contemporary ceramic vase perfect for fresh or dried flowers",
"meta_keywords": "vase, ceramic vase, flower vase, white, home decor",
"description": [
{
"id": "r__vase_white",
"columns": [
{
"size": 1,
"id": "c__vase_white",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "vase_white_p1",
"type": "paragraph",
"data": {
"text": "Elevate your home decor with our Modern Ceramic Vase. The sleek, contemporary design complements any interior style, from minimalist to traditional."
}
},
{
"id": "vase_white_p2",
"type": "paragraph",
"data": {
"text": "Perfect for displaying fresh flowers, dried arrangements, or as a standalone decorative piece. The sturdy ceramic construction ensures long-lasting beauty."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 60,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "new-arrivals"],
"category": "accessories",
"variant_group": "modern-ceramic-vase",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/vase-white.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "White"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "VASE-004-BLK",
"name": "Modern Ceramic Vase - Black",
"url_key": "modern-ceramic-vase-black",
"price": 25,
"weight": 500,
"meta_title": "Modern Ceramic Vase - Black",
"meta_description": "Contemporary ceramic vase perfect for fresh or dried flowers",
"meta_keywords": "vase, ceramic vase, flower vase, black, home decor",
"description": [
{
"id": "r__vase_black",
"columns": [
{
"size": 1,
"id": "c__vase_black",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "vase_black_p1",
"type": "paragraph",
"data": {
"text": "Elevate your home decor with our Modern Ceramic Vase. The sleek, contemporary design complements any interior style, from minimalist to traditional."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 60,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "new-arrivals"],
"category": "accessories",
"variant_group": "modern-ceramic-vase",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/vase-black.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Black"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "VASE-004-YEL",
"name": "Modern Ceramic Vase - Green",
"url_key": "modern-ceramic-vase-green",
"price": 25,
"weight": 500,
"meta_title": "Modern Ceramic Vase - Green",
"meta_description": "Contemporary ceramic vase perfect for fresh or dried flowers",
"meta_keywords": "vase, ceramic vase, flower vase, green, home decor",
"description": [
{
"id": "r__vase_green",
"columns": [
{
"size": 1,
"id": "c__vase_green",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "vase_green_p1",
"type": "paragraph",
"data": {
"text": "Elevate your home decor with our Modern Ceramic Vase. The sleek, contemporary design complements any interior style, from minimalist to traditional."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 60,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "new-arrivals"],
"category": "accessories",
"variant_group": "modern-ceramic-vase",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/vase-green.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Green"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "THERMO-005-WHT",
"name": "Stainless Steel Thermos - White",
"url_key": "stainless-steel-thermos-white",
"price": 35,
"weight": 350,
"meta_title": "Stainless Steel Thermos - White",
"meta_description": "Insulated stainless steel thermos keeps drinks hot or cold for hours",
"meta_keywords": "thermos, insulated bottle, water bottle, white, drinkware",
"description": [
{
"id": "r__thermo_white",
"columns": [
{
"size": 1,
"id": "c__thermo_white",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "thermo_white_p1",
"type": "paragraph",
"data": {
"text": "Keep your beverages at the perfect temperature with our Stainless Steel Thermos. Double-wall vacuum insulation keeps drinks hot for 12 hours or cold for 24 hours."
}
},
{
"id": "thermo_white_p2",
"type": "paragraph",
"data": {
"text": "The leak-proof lid and durable stainless steel construction make it perfect for travel, work, or outdoor activities. BPA-free and easy to clean."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 120,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "stainless-steel-thermos",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/thermos-white.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "White"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "THERMO-005-BLK",
"name": "Stainless Steel Thermos - Black",
"url_key": "stainless-steel-thermos-black",
"price": 35,
"weight": 350,
"meta_title": "Stainless Steel Thermos - Black",
"meta_description": "Insulated stainless steel thermos keeps drinks hot or cold for hours",
"meta_keywords": "thermos, insulated bottle, water bottle, black, drinkware",
"description": [
{
"id": "r__thermo_black",
"columns": [
{
"size": 1,
"id": "c__thermo_black",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "thermo_black_p1",
"type": "paragraph",
"data": {
"text": "Keep your beverages at the perfect temperature with our Stainless Steel Thermos. Double-wall vacuum insulation keeps drinks hot for 12 hours or cold for 24 hours."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 120,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "stainless-steel-thermos",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/thermos-black.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Black"
}
]
},
{
"type": "simple",
"visibility": true,
"status": true,
"sku": "THERMO-005-YEL",
"name": "Stainless Steel Thermos - Yellow",
"url_key": "stainless-steel-thermos-yellow",
"price": 35,
"weight": 350,
"meta_title": "Stainless Steel Thermos - Yellow",
"meta_description": "Insulated stainless steel thermos keeps drinks hot or cold for hours",
"meta_keywords": "thermos, insulated bottle, water bottle, yellow, drinkware",
"description": [
{
"id": "r__thermo_yellow",
"columns": [
{
"size": 1,
"id": "c__thermo_yellow",
"data": {
"time": 1729900000000,
"blocks": [
{
"id": "thermo_yellow_p1",
"type": "paragraph",
"data": {
"text": "Keep your beverages at the perfect temperature with our Stainless Steel Thermos. Double-wall vacuum insulation keeps drinks hot for 12 hours or cold for 24 hours."
}
}
],
"version": "2.31.0"
}
}
],
"size": 1,
"className": "md:grid-cols-1"
}
],
"qty": 120,
"manage_stock": true,
"stock_availability": true,
"collections": ["homepage", "best-sellers"],
"category": "accessories",
"variant_group": "stainless-steel-thermos",
"images": [
{
"url": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/thermos-yellow.jpg",
"isMain": true
}
],
"attributes": [
{
"attribute_code": "color",
"value": "Yellow"
}
]
}
]
================================================
FILE: packages/evershop/src/bin/seed/data/widgets.json
================================================
[
{
"name": "Main menu",
"type": "basic_menu",
"route": ["all"],
"area": ["headerMiddleLeft"],
"sort_order": 1,
"settings": {
"menus": [
{
"id": "hanhk3km0m8nt2b",
"url": "#",
"name": "Shop",
"type": "custom",
"uuid": "#",
"children": [
{
"id": "hanhk3km0m8nt2c",
"url": "/accessories",
"name": "Accessories",
"type": "custom",
"uuid": "/accessories"
}
]
},
{
"id": "hanhk3km0m8nt2e",
"url": "/page/about-us",
"name": "About us",
"type": "custom",
"uuid": "/page/about-us",
"children": []
}
],
"isMain": "1",
"className": ""
},
"status": true
},
{
"name": "Homepage Slideshow",
"type": "simple_slider",
"route": ["homepage"],
"area": ["content"],
"sort_order": 5,
"settings": {
"dots": true,
"arrows": true,
"slides": [
{
"id": "slide-1",
"image": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/banner-one.jpg",
"width": 2400,
"height": 1200,
"subText": "Discover our exquisite collection of ceramic and stainless steel products",
"headline": "Premium Quality Products",
"buttonLink": "/accessories",
"buttonText": "Shop Now",
"buttonColor": "#3a3a3a"
},
{
"id": "slide-2",
"image": "https://raw.githubusercontent.com/evershopcommerce/evershop/refs/heads/dev/seed/images/banner-two.jpg",
"width": 2400,
"height": 1200,
"subText": "Elegant designs that enhance your daily life, from morning coffee to home organization",
"headline": "Crafted With Care",
"buttonLink": "/accessories",
"buttonText": "View Collection",
"buttonColor": "#3a3a3a"
}
],
"autoplay": true,
"fullWidth": true,
"heightType": "auto",
"autoplaySpeed": 3000
},
"status": true
},
{
"name": "Featured Products",
"type": "collection_products",
"route": ["homepage"],
"area": ["content"],
"sort_order": 20,
"settings": {
"count": 4,
"collection": "homepage"
},
"status": true
}
]
================================================
FILE: packages/evershop/src/bin/seed/imageDownloader.ts
================================================
import { createWriteStream, existsSync, mkdirSync } from 'fs';
import http from 'http';
import https from 'https';
import { dirname } from 'path';
import { pipeline } from 'stream/promises';
import { info, warning } from '../../lib/log/logger.js';
/**
* Download an image from a URL and save it to a local file
*/
export async function downloadImage(
url: string,
outputPath: string
): Promise {
return new Promise((resolve, reject) => {
// Ensure directory exists
const dir = dirname(outputPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
const client = url.startsWith('https') ? https : http;
const request = client.get(url, (response) => {
// Handle redirects
if (
response.statusCode === 301 ||
response.statusCode === 302 ||
response.statusCode === 307 ||
response.statusCode === 308
) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
info(` → Following redirect to: ${redirectUrl}`);
downloadImage(redirectUrl, outputPath).then(resolve).catch(reject);
return;
}
}
if (response.statusCode !== 200) {
reject(
new Error(`Failed to download: HTTP ${response.statusCode} - ${url}`)
);
return;
}
const fileStream = createWriteStream(outputPath);
pipeline(response, fileStream)
.then(() => {
info(` ✓ Downloaded: ${url} → ${outputPath}`);
resolve(outputPath);
})
.catch((err) => {
reject(new Error(`Failed to save file: ${err.message}`));
});
});
request.on('error', (err) => {
reject(new Error(`Download failed: ${err.message}`));
});
request.setTimeout(30000, () => {
request.destroy();
reject(new Error('Download timeout'));
});
});
}
/**
* Generate a filename from URL
*/
export function getFilenameFromUrl(url: string): string {
try {
const urlObj = new URL(url);
// For Unsplash images, extract photo ID
if (urlObj.hostname.includes('unsplash.com')) {
const photoId = urlObj.pathname.split('/').pop() || 'image';
return `${photoId}.jpg`;
}
const pathname = urlObj.pathname;
const filename = pathname.split('/').pop() || 'image.jpg';
// Ensure it has an extension
if (!filename.includes('.')) {
return `${filename}.jpg`;
}
return filename;
} catch {
return `image-${Date.now()}.jpg`;
}
}
/**
* Convert GitHub raw URL to a local media path
*/
export function convertToMediaPath(localPath: string): string {
// Convert absolute path to relative media path
// e.g., /path/to/media/widgets/slide-1.jpg -> /assets/widgets/slide-1.jpg
// or on Windows: C:\path\to\media\widgets\slide-1.jpg -> /assets/widgets/slide-1.jpg
// Normalize to forward slashes for consistent matching
const normalizedPath = localPath.replace(/\\/g, '/');
const mediaMatch = normalizedPath.match(/media\/(.+)$/);
if (mediaMatch) {
return `/assets/${mediaMatch[1]}`;
}
return localPath;
}
================================================
FILE: packages/evershop/src/bin/seed/index.ts
================================================
/* eslint-disable no-console */
import './initEnvDev.js';
import 'dotenv/config';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { error, success, info } from '../../lib/log/logger.js';
import { seedAttributeGroup, seedAttributes } from './seedAttributes.js';
import { seedCategories } from './seedCategories.js';
import { seedCollections } from './seedCollections.js';
import { seedPages } from './seedPages.js';
import { seedProducts } from './seedProducts.js';
import { seedWidgets } from './seedWidgets.js';
const { argv } = yargs(hideBin(process.argv))
.option('attributes', {
alias: 'a',
description: 'Seed product attributes',
type: 'boolean',
default: false
})
.option('categories', {
alias: 'c',
description: 'Seed categories',
type: 'boolean',
default: false
})
.option('collections', {
alias: 'col',
description: 'Seed collections',
type: 'boolean',
default: false
})
.option('products', {
alias: 'p',
description: 'Seed products',
type: 'boolean',
default: false
})
.option('widgets', {
alias: 'w',
description: 'Seed widgets',
type: 'boolean',
default: false
})
.option('pages', {
alias: 'pg',
description: 'Seed CMS pages',
type: 'boolean',
default: false
})
.option('all', {
description:
'Seed all demo data (attributes, categories, collections, products, widgets, pages)',
type: 'boolean',
default: false
})
.check((argv) => {
if (
!argv.attributes &&
!argv.categories &&
!argv.collections &&
!argv.products &&
!argv.widgets &&
!argv.pages &&
!argv.all
) {
throw new Error(
'Please specify at least one option: --attributes, --categories, --collections, --products, --widgets, --pages, or --all'
);
}
return true;
})
.help();
interface SeedOptions {
attributes: boolean;
categories: boolean;
collections: boolean;
products: boolean;
widgets: boolean;
pages: boolean;
all: boolean;
}
async function seed() {
const options = argv as unknown as SeedOptions;
let demoAttributeGroupId: number | null = null;
try {
info('Starting demo data seeding...\n');
// Create attribute group first if we're seeding attributes or products
if (options.all || options.attributes || options.products) {
demoAttributeGroupId = await seedAttributeGroup();
console.log();
}
if (options.all || options.attributes) {
if (!demoAttributeGroupId) {
demoAttributeGroupId = await seedAttributeGroup();
}
await seedAttributes(demoAttributeGroupId);
console.log();
}
if (options.all || options.categories) {
await seedCategories();
console.log();
}
if (options.all || options.collections) {
await seedCollections();
console.log();
}
if (options.all || options.products) {
if (!demoAttributeGroupId) {
demoAttributeGroupId = await seedAttributeGroup();
}
await seedProducts(demoAttributeGroupId);
console.log();
}
if (options.all || options.widgets) {
await seedWidgets();
console.log();
}
if (options.all || options.pages) {
await seedPages();
console.log();
}
success('✓ Demo data seeding completed successfully!');
process.exit(0);
} catch (e: any) {
error(`Seeding failed: ${e.message}`);
process.exit(1);
}
}
seed();
================================================
FILE: packages/evershop/src/bin/seed/initEnvDev.ts
================================================
import 'dotenv/config';
process.env.NODE_ENV = 'development';
================================================
FILE: packages/evershop/src/bin/seed/seedAttributes.ts
================================================
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { insert, select } from '@evershop/postgres-query-builder';
import { info, success, error } from '../../lib/log/logger.js';
import { pool } from '../../lib/postgres/connection.js';
import createProductAttribute from '../../modules/catalog/services/attribute/createProductAttribute.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Create or get the demo attribute group
*/
export async function seedAttributeGroup(): Promise {
info('Creating demo attribute group...');
// Check if demo group already exists
const existingGroup = await select()
.from('attribute_group')
.where('group_name', '=', 'Demo Products')
.load(pool);
if (existingGroup) {
info('Demo attribute group already exists, reusing...');
return existingGroup.attribute_group_id;
}
// Create the demo attribute group
const result = await insert('attribute_group')
.given({
group_name: 'Demo Products'
})
.execute(pool);
success(`✓ Created attribute group: Demo Products (ID: ${result.insertId})`);
return result.insertId;
}
/**
* Seed product attributes from JSON file
*/
export async function seedAttributes(
demoAttributeGroupId: number
): Promise {
info('Seeding attributes...');
const dataPath = path.join(__dirname, 'data', 'attributes.json');
const attributesData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
for (const attributeData of attributesData) {
try {
// Check if attribute already exists
const existingAttribute = await select()
.from('attribute')
.where('attribute_code', '=', attributeData.attribute_code)
.load(pool);
if (existingAttribute) {
info(
`Attribute "${attributeData.attribute_name}" already exists, updating options...`
);
// If attribute has options (select/multiselect type), sync the options
if (attributeData.options && Array.isArray(attributeData.options)) {
for (const optionData of attributeData.options) {
// Check if option already exists
const existingOption = await select()
.from('attribute_option')
.where('attribute_id', '=', existingAttribute.attribute_id)
.and('option_text', '=', optionData.option_text)
.load(pool);
if (!existingOption) {
// Add new option - must include attribute_code
await insert('attribute_option')
.given({
attribute_id: existingAttribute.attribute_id,
attribute_code: existingAttribute.attribute_code,
option_text: optionData.option_text
})
.execute(pool);
success(` ✓ Added option: ${optionData.option_text}`);
} else {
info(` → Option "${optionData.option_text}" already exists`);
}
}
}
// Ensure attribute is linked to demo group
const existingLink = await select()
.from('attribute_group_link')
.where('attribute_id', '=', existingAttribute.attribute_id)
.and('group_id', '=', demoAttributeGroupId)
.load(pool);
if (!existingLink) {
await insert('attribute_group_link')
.given({
attribute_id: existingAttribute.attribute_id,
group_id: demoAttributeGroupId
})
.execute(pool);
info(` → Linked to Demo Products group`);
}
continue;
}
// Add the demo group if no groups specified
if (!attributeData.groups || attributeData.groups.length === 0) {
attributeData.groups = [demoAttributeGroupId];
}
await createProductAttribute(attributeData, {});
success(`✓ Created attribute: ${attributeData.attribute_name}`);
} catch (e: any) {
error(
`Failed to create attribute ${attributeData.attribute_name}: ${e.message}`
);
}
}
}
================================================
FILE: packages/evershop/src/bin/seed/seedCategories.ts
================================================
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { select } from '@evershop/postgres-query-builder';
import { info, success, error } from '../../lib/log/logger.js';
import { pool } from '../../lib/postgres/connection.js';
import createCategory from '../../modules/catalog/services/category/createCategory.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Seed categories from JSON file
*/
export async function seedCategories(): Promise {
info('Seeding categories...');
const dataPath = path.join(__dirname, 'data', 'categories.json');
const categoriesData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
for (const categoryData of categoriesData) {
try {
// Check if category already exists
const existingCategory = await select()
.from('category_description')
.where('url_key', '=', categoryData.url_key)
.load(pool);
if (existingCategory) {
info(`Category "${categoryData.name}" already exists, skipping...`);
continue;
}
await createCategory(categoryData, {});
success(`✓ Created category: ${categoryData.name}`);
} catch (e: any) {
error(`Failed to create category ${categoryData.name}: ${e.message}`);
}
}
}
================================================
FILE: packages/evershop/src/bin/seed/seedCollections.ts
================================================
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { select } from '@evershop/postgres-query-builder';
import { info, success, error } from '../../lib/log/logger.js';
import { pool } from '../../lib/postgres/connection.js';
import createCollection from '../../modules/catalog/services/collection/createCollection.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Seed collections from JSON file
*/
export async function seedCollections(): Promise {
info('Seeding collections...');
const dataPath = path.join(__dirname, 'data', 'collections.json');
const collectionsData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
for (const collectionData of collectionsData) {
try {
// Check if collection already exists
const existingCollection = await select()
.from('collection')
.where('code', '=', collectionData.code)
.load(pool);
if (existingCollection) {
info(`Collection "${collectionData.name}" already exists, skipping...`);
continue;
}
await createCollection(collectionData, {});
success(`✓ Created collection: ${collectionData.name}`);
} catch (e: any) {
error(`Failed to create collection ${collectionData.name}: ${e.message}`);
}
}
}
================================================
FILE: packages/evershop/src/bin/seed/seedImages.ts
================================================
import { existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { insert, select } from '@evershop/postgres-query-builder';
import { CONSTANTS } from '../../lib/helpers.js';
import { info, success, warning, error } from '../../lib/log/logger.js';
import { pool } from '../../lib/postgres/connection.js';
import { downloadImage, getFilenameFromUrl } from './imageDownloader.js';
/**
* Seed product images by downloading from GitHub raw URLs
*/
export async function seedProductImages(
productId: number,
images: any[]
): Promise {
if (!images || images.length === 0) return;
for (let i = 0; i < images.length; i++) {
const imageData = images[i];
try {
let finalImageUrl = imageData.url;
// Download image if it's a remote URL
if (imageData.url && imageData.url.startsWith('http')) {
info(` → Downloading image: ${imageData.url}`);
// Get filename from URL
const filename = getFilenameFromUrl(imageData.url);
// Create local path - organize by SKU
const subPath = `catalog/${
Math.floor(Math.random() * (9999 - 1000)) + 1000
}/${Math.floor(Math.random() * (9999 - 1000)) + 1000}`;
const mediaDir = join(CONSTANTS.ROOTPATH, 'media', subPath);
// Ensure directory exists
if (!existsSync(mediaDir)) {
mkdirSync(mediaDir, { recursive: true });
}
const localPath = join(mediaDir, filename);
try {
// Download image
await downloadImage(imageData.url, localPath);
// Convert to media URL
finalImageUrl = `/assets/${subPath}/${filename}`;
success(` ✓ Downloaded and saved: ${mediaDir}`);
// Check if image record already exists
const existingImage = await select()
.from('product_image')
.where('product_image_product_id', '=', productId)
.and('origin_image', '=', finalImageUrl)
.load(pool);
if (!existingImage) {
// Save image URL to database
await insert('product_image')
.given({
product_image_product_id: productId,
origin_image: finalImageUrl,
is_main: imageData.isMain ? 1 : 0
})
.execute(pool);
info(` ✓ Added image record to database`);
} else {
info(` → Image already exists in database`);
}
} catch (downloadErr: any) {
error(` ✗ Failed to download image: ${downloadErr.message}`);
}
}
} catch (e: any) {
warning(` ⚠️ Failed to process image ${i + 1}: ${e.message}`);
}
}
}
================================================
FILE: packages/evershop/src/bin/seed/seedPages.ts
================================================
import { readFileSync } from 'fs';
import { join } from 'path';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { insert, select } from '@evershop/postgres-query-builder';
import { error, info, success } from '../../lib/log/logger.js';
import { getConnection } from '../../lib/postgres/connection.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
interface PageData {
status: boolean;
url_key: string;
name: string;
content: any[];
meta_title: string;
meta_keywords?: string;
meta_description?: string;
}
/**
* Seed CMS pages from JSON file
*/
export async function seedPages(): Promise {
try {
info('Seeding CMS pages...');
// Read pages data
const pagesPath = join(__dirname, 'data', 'pages.json');
const pagesData: PageData[] = JSON.parse(readFileSync(pagesPath, 'utf-8'));
const connection = await getConnection();
let created = 0;
let skipped = 0;
for (const pageData of pagesData) {
// Check if page already exists (by url_key)
const existing = await select()
.from('cms_page_description')
.where('url_key', '=', pageData.url_key)
.load(connection, false);
if (existing) {
info(` ⊘ Page "${pageData.url_key}" already exists, skipping...`);
skipped++;
continue;
}
// Insert cms_page first
const page = await insert('cms_page')
.given({
status: pageData.status
})
.execute(connection, false);
// Insert cms_page_description
await insert('cms_page_description')
.given({
cms_page_description_cms_page_id: page.cms_page_id,
url_key: pageData.url_key,
name: pageData.name,
content: JSON.stringify(pageData.content),
meta_title: pageData.meta_title,
meta_keywords: pageData.meta_keywords || null,
meta_description: pageData.meta_description || null
})
.execute(connection);
success(` ✓ Created page: ${pageData.name} (/${pageData.url_key})`);
created++;
}
success(
`✓ CMS pages seeding complete: ${created} created, ${skipped} skipped`
);
} catch (e: any) {
error(`Failed to seed pages: ${e.message}`);
throw e;
}
}
================================================
FILE: packages/evershop/src/bin/seed/seedProducts.ts
================================================
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { insert, select } from '@evershop/postgres-query-builder';
import { info, success, error, warning } from '../../lib/log/logger.js';
import { pool } from '../../lib/postgres/connection.js';
import createProduct from '../../modules/catalog/services/product/createProduct.js';
import { seedProductImages } from './seedImages.js';
import {
createVariantGroups,
resolveAttributeOptions
} from './variantGroupHelpers.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Seed products from JSON file
*/
export async function seedProducts(
demoAttributeGroupId: number
): Promise {
info('Seeding products...');
const dataPath = path.join(__dirname, 'data', 'products.json');
const productsData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
// Get color and size attribute IDs
const colorAttribute = await select()
.from('attribute')
.where('attribute_code', '=', 'color')
.load(pool);
const sizeAttribute = await select()
.from('attribute')
.where('attribute_code', '=', 'size')
.load(pool);
if (!colorAttribute || !sizeAttribute) {
error(
'Color and Size attributes must be seeded first. Run: npm run seed -- --attributes'
);
return;
}
// Create variant groups
const variantGroupIds = await createVariantGroups(
productsData,
demoAttributeGroupId,
colorAttribute.attribute_id
);
// Seed products
info('\nSeeding products...');
for (const productData of productsData) {
try {
// Check if product already exists
const existingProduct = await select()
.from('product')
.where('sku', '=', productData.sku)
.load(pool);
if (existingProduct) {
info(
`Product "${productData.name}" (${productData.sku}) already exists, skipping...`
);
continue;
}
// Assign product to the demo attribute group
if (!productData.group_id) {
if (!demoAttributeGroupId) {
error('Demo attribute group ID is not set. This should not happen.');
continue;
}
productData.group_id = demoAttributeGroupId;
}
// Resolve category_id from the category field
if (productData.category) {
const categoryUrlKey = productData.category;
const categoryQuery = select('category.category_id').from(
'category_description'
);
categoryQuery
.leftJoin('category')
.on(
'category.category_id',
'=',
'category_description.category_description_category_id'
);
categoryQuery.where(
'category_description.url_key',
'=',
categoryUrlKey
);
const category = await categoryQuery.load(pool);
if (category && category.category_id) {
productData.category_id = category.category_id;
} else {
warning(
` ⚠️ Category "${categoryUrlKey}" not found, product will have no category`
);
}
// Remove category field as it's not needed for product creation
delete productData.category;
}
// Save collections, images, and variant_group for later processing
const collections = productData.collections;
const images = productData.images;
const variantGroup = productData.variant_group;
delete productData.collections;
delete productData.images;
delete productData.variant_group;
// Set variant_group_id if this product belongs to a variant group
if (variantGroup && variantGroupIds.has(variantGroup)) {
productData.variant_group_id = variantGroupIds.get(variantGroup);
info(
` → Assigning to variant group: ${variantGroup} (ID: ${productData.variant_group_id})`
);
}
// Convert attribute values to option IDs for select type attributes
if (productData.attributes && Array.isArray(productData.attributes)) {
productData.attributes = await resolveAttributeOptions(
productData.attributes
);
}
const product = await createProduct(productData, {});
success(`✓ Created product: ${productData.name} (${productData.sku})`);
// Process images
if (images && Array.isArray(images)) {
await seedProductImages(product.insertId, images);
}
// Assign product to collections if specified
if (collections && Array.isArray(collections)) {
for (const collectionCode of collections) {
const collection = await select()
.from('collection')
.where('code', '=', collectionCode)
.load(pool);
if (collection) {
await insert('product_collection')
.given({
collection_id: collection.collection_id,
product_id: product.insertId
})
.execute(pool);
info(` → Assigned to collection: ${collectionCode}`);
}
}
}
} catch (e: any) {
error(`Failed to create product ${productData.name}: ${e.message}`);
}
}
}
================================================
FILE: packages/evershop/src/bin/seed/seedWidgets.ts
================================================
import { readFileSync, existsSync, mkdirSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { insert, select } from '@evershop/postgres-query-builder';
import { CONSTANTS } from '../../lib/helpers.js';
import { error, info, success } from '../../lib/log/logger.js';
import { getConnection } from '../../lib/postgres/connection.js';
import {
downloadImage,
getFilenameFromUrl,
convertToMediaPath
} from './imageDownloader.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
interface WidgetData {
name: string;
type: string;
status: 1 | 0;
area: string;
route: string[];
settings: Record;
sort_order: number;
}
interface SlideData {
id: string;
image: string;
width: number;
height: number;
headline?: string;
subheadline?: string;
buttonText?: string;
buttonUrl?: string;
}
/**
* Download slideshow images and update URLs
*/
async function downloadSlideshowImages(
settings: Record
): Promise> {
if (settings.slides && Array.isArray(settings.slides)) {
const updatedSlides: SlideData[] = [];
for (const slide of settings.slides as SlideData[]) {
if (slide.image && slide.image.startsWith('http')) {
try {
info(` → Downloading slide image: ${slide.image}`);
// Get filename from URL
const filename = getFilenameFromUrl(slide.image);
const slideId = slide.id || `slide-${Date.now()}`;
// Create local path
const mediaDir = join(
CONSTANTS.ROOTPATH,
'media',
'widgets',
slideId
);
// Ensure directory exists
if (!existsSync(mediaDir)) {
mkdirSync(mediaDir, { recursive: true });
}
const localPath = join(mediaDir, filename);
// Download image
await downloadImage(slide.image, localPath);
// Convert to media URL
const mediaUrl = convertToMediaPath(localPath);
// Update slide with local URL
updatedSlides.push({
...slide,
image: mediaUrl
});
} catch (err) {
error(` ✗ Failed to download slide image: ${err}`);
// Keep original URL on failure
updatedSlides.push(slide);
}
} else {
updatedSlides.push(slide);
}
}
return {
...settings,
slides: updatedSlides
};
}
return settings;
}
/**
* Seed widgets from JSON file
*/
export async function seedWidgets(): Promise {
try {
info('Seeding widgets...');
// Read widgets data
const widgetsPath = join(__dirname, 'data', 'widgets.json');
const widgetsData: WidgetData[] = JSON.parse(
readFileSync(widgetsPath, 'utf-8')
);
const connection = await getConnection();
let created = 0;
let skipped = 0;
for (const widgetData of widgetsData) {
// Check if widget already exists (by name and type)
const existing = await select()
.from('widget')
.where('name', '=', widgetData.name)
.and('type', '=', widgetData.type)
.load(connection, false);
if (existing) {
info(` ⊘ Widget "${widgetData.name}" already exists, skipping...`);
skipped++;
continue;
}
// Process settings - download slideshow images if needed
let processedSettings = widgetData.settings;
if (widgetData.type === 'simple_slider') {
info(` → Processing slideshow images for: ${widgetData.name}`);
processedSettings = await downloadSlideshowImages(widgetData.settings);
}
// Insert widget
await insert('widget')
.given({
name: widgetData.name,
type: widgetData.type,
area: widgetData.area,
route: JSON.stringify(widgetData.route),
sort_order: widgetData.sort_order,
settings: JSON.stringify(processedSettings),
status: widgetData.status
})
.execute(connection, false);
success(` ✓ Created widget: ${widgetData.name}`);
created++;
}
success(
`✓ Widget seeding complete: ${created} created, ${skipped} skipped`
);
} catch (e: any) {
error(`Failed to seed widgets: ${e.message}`);
throw e;
}
}
================================================
FILE: packages/evershop/src/bin/seed/variantGroupHelpers.ts
================================================
import { insert, select } from '@evershop/postgres-query-builder';
import { v4 as uuidv4 } from 'uuid';
import { info, success, error } from '../../lib/log/logger.js';
import { pool } from '../../lib/postgres/connection.js';
/**
* Create variant groups for products
*/
export async function createVariantGroups(
productsData: any[],
demoAttributeGroupId: number,
colorAttributeId: number
): Promise