├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── pull_request_review.yml │ ├── pull_request_title_check.yml │ ├── release.yml │ └── release_image.yml ├── .gitignore ├── .husky └── commit-msg ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .releaserc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── stencil-bundle.js ├── stencil-debug.js ├── stencil-download.js ├── stencil-init.js ├── stencil-pull.js ├── stencil-push.js ├── stencil-release.js ├── stencil-scss-autofix.js ├── stencil-start.js └── stencil.js ├── commitlint.config.cjs ├── constants.js ├── jest.config.js ├── lib ├── BuildConfigManager.js ├── BuildConfigManager.spec.js ├── Cycles.js ├── Cycles.spec.js ├── ScssValidator.js ├── ScssValidator.spec.js ├── StencilCLISettings.js ├── StencilCLISettings.spec.js ├── StencilConfigManager.js ├── StencilConfigManager.spec.js ├── StencilDebug.js ├── StencilDebug.spec.js ├── archiveManager.js ├── archiveManager.spec.js ├── bundle-validator.js ├── bundle-validator.spec.js ├── cliCommon.js ├── cliCommon.spec.js ├── commander.js ├── content-api-client.js ├── css │ └── compile.js ├── graphql │ └── query.js ├── lang-assembler.js ├── lang-assembler.spec.js ├── lang-helper.js ├── lang │ ├── validator.js │ └── validator.spec.js ├── nodeSass │ ├── AutoFixer.js │ ├── BaseFixer.js │ ├── BaseRulesFixer.js │ ├── CommaRemovalFixer.js │ ├── ConditionalImportFixer.js │ └── UndefinedVariableFixer.js ├── parse-json.js ├── parse-json.spec.js ├── regions.js ├── regions.spec.js ├── release │ ├── questions.js │ ├── release.js │ └── release.spec.js ├── schemas │ ├── privateThemeConfig.json │ ├── schemaTranslations.json │ ├── themeConfig.json │ └── themeSchema.json ├── spinner.js ├── stencil-bundle.js ├── stencil-bundle.spec.js ├── stencil-download.js ├── stencil-download.utils.js ├── stencil-init.js ├── stencil-init.spec.js ├── stencil-pull.js ├── stencil-pull.utils.js ├── stencil-push.js ├── stencil-push.utils.js ├── stencil-push.utils.spec.js ├── stencil-start.js ├── stencil-start.spec.js ├── store-settings-api-client.js ├── template-assembler.js ├── theme-api-client.js ├── theme-config.js ├── theme-config.spec.js ├── utils │ ├── NetworkUtils.js │ ├── NetworkUtils.spec.js │ ├── asyncUtils.js │ ├── frontmatter.js │ ├── frontmatter.spec.js │ └── fsUtils.js └── validator │ ├── schema-translations.js │ └── schema-translations.spec.js ├── package-lock.json ├── package.json ├── server ├── config.js ├── index.js ├── lib │ ├── page-type-util.js │ ├── page-type-util.spec.js │ ├── show-logo.js │ ├── utils.js │ └── utils.spec.js ├── manifest.js └── plugins │ ├── renderer │ ├── renderer.module.js │ ├── renderer.module.spec.js │ └── responses │ │ ├── index.js │ │ ├── pencil-response.js │ │ ├── pencil-response.spec.js │ │ ├── raw-response.js │ │ ├── raw-response.spec.js │ │ └── redirect-response.js │ ├── router │ ├── router.module.js │ └── router.module.spec.js │ └── theme-assets │ └── theme-assets.module.js └── test ├── _mocks ├── MockWritableStream.js ├── api │ ├── getConfigurations.schema.json │ ├── getVariations.schema.json │ ├── getVersions.schema.json │ └── postConfigurations.schema.json ├── build-config │ ├── legacy-config │ │ └── stencil.conf.cjs │ ├── noready-config │ │ └── stencil.conf.cjs │ ├── noworker-config │ │ └── stencil.conf.cjs │ └── valid-config │ │ └── stencil.conf.cjs ├── frontmatter │ ├── absent.html │ └── valid.html ├── malformedSchema.json └── themes │ ├── bad-schema │ ├── config.json │ └── schema.json │ ├── bare-bones │ └── config.json │ ├── component-with-external-template │ ├── a.html │ └── b.html │ ├── invalid-frontmatter │ ├── config.json │ ├── lang │ │ └── en.json │ ├── schema.json │ ├── schemaTranslations.json │ └── templates │ │ ├── components │ │ ├── a.html │ │ └── b.html │ │ └── pages │ │ ├── page.html │ │ └── page2.html │ ├── invalid-schema │ ├── config.json │ ├── meta │ │ ├── composed.jpg │ │ ├── desktop_bold.jpg │ │ ├── desktop_light.jpg │ │ ├── desktop_warm.jpg │ │ ├── mobile_bold.jpg │ │ ├── mobile_light.jpg │ │ └── mobile_warm.jpg │ └── schema.json │ ├── invalid-scss-latest-node-sass-to-fix │ ├── assets │ │ └── scss │ │ │ ├── test.scss │ │ │ └── theme.scss │ ├── config.json │ ├── config.stencil.json │ ├── lang │ │ └── en.json │ ├── meta │ │ ├── composed.jpg │ │ ├── desktop_bold.jpg │ │ ├── desktop_light.jpg │ │ ├── desktop_warm.jpg │ │ ├── mobile_bold.jpg │ │ ├── mobile_light.jpg │ │ └── mobile_warm.jpg │ ├── mock-theme.zip │ ├── schema.json │ ├── schemaTranslations.json │ ├── secrets.stencil.json │ └── templates │ │ ├── components │ │ ├── a.html │ │ └── b.html │ │ └── pages │ │ ├── page.html │ │ └── page2.html │ ├── invalid-scss-latest-node-sass │ ├── assets │ │ └── scss │ │ │ ├── test.scss │ │ │ └── theme.scss │ ├── config.json │ ├── config.stencil.json │ ├── lang │ │ └── en.json │ ├── meta │ │ ├── composed.jpg │ │ ├── desktop_bold.jpg │ │ ├── desktop_light.jpg │ │ ├── desktop_warm.jpg │ │ ├── mobile_bold.jpg │ │ ├── mobile_light.jpg │ │ └── mobile_warm.jpg │ ├── mock-theme.zip │ ├── schema.json │ ├── schemaTranslations.json │ ├── secrets.stencil.json │ └── templates │ │ ├── components │ │ ├── a.html │ │ └── b.html │ │ └── pages │ │ ├── page.html │ │ └── page2.html │ ├── invalid-translations │ ├── lang │ │ └── en.json │ └── templates │ │ ├── components │ │ ├── a.html │ │ └── b.html │ │ └── pages │ │ ├── page.html │ │ └── page2.html │ ├── missing-variation │ └── config.json │ ├── regions │ └── templates │ │ ├── components │ │ ├── bottom.html │ │ ├── dynamic │ │ │ ├── a.html │ │ │ ├── b.html │ │ │ └── c.html │ │ ├── middle.html │ │ ├── other.html │ │ └── top.html │ │ ├── layout │ │ └── base.html │ │ └── pages │ │ └── page.html │ └── valid │ ├── assets │ ├── custom │ │ └── css │ │ │ └── test.css │ └── scss │ │ ├── checkout.scss │ │ └── theme.scss │ ├── config.json │ ├── config.stencil.json │ ├── lang │ └── en.json │ ├── meta │ ├── composed.jpg │ ├── desktop_bold.jpg │ ├── desktop_light.jpg │ ├── desktop_warm.jpg │ ├── mobile_bold.jpg │ ├── mobile_light.jpg │ └── mobile_warm.jpg │ ├── mock-theme.zip │ ├── schema.json │ ├── schemaTranslations.json │ ├── secrets.stencil.json │ └── templates │ ├── components │ ├── a.html │ ├── b.html │ ├── c.html │ ├── li.html │ └── ul.html │ └── pages │ ├── page.html │ ├── page2.html │ └── page3.html ├── assertions └── assertNoMutations.js └── assets └── cat_and_dog.jpeg /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | [*.json] 13 | indent_size = 2 14 | [.eslintrc] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaFeatures": { 5 | "impliedStrict": true 6 | }, 7 | "ecmaVersion": "2021" 8 | }, 9 | "plugins": ["jest", "node", "prettier"], 10 | "extends": [ 11 | "eslint:recommended", 12 | "airbnb-base", 13 | "plugin:node/recommended", 14 | "plugin:jest/recommended", 15 | "plugin:jest/style", 16 | "prettier", 17 | "plugin:prettier/recommended" 18 | ], 19 | "env": { 20 | "es2020": true, 21 | "jest/globals": true, 22 | "node": true 23 | }, 24 | "rules": { 25 | ///////////////////////////////////////// Our rules ///////////////////////////////////////// 26 | "no-eq-null": "error", 27 | 28 | ///////////////////////////////////////// Overrides //////////////////////////////////////// 29 | "prettier/prettier": "warn", 30 | "no-console": "off", 31 | "class-methods-use-this": "off", 32 | "no-continue": "off", 33 | // Overwrites airbnb-base because our Node version doesn't allow to use private properies syntax yet, so we use underscores 34 | "no-underscore-dangle": ["error", { "allowAfterThis": true }], 35 | // Overwrites airbnb-base to allow for..of: 36 | "no-restricted-syntax": [ 37 | "error", 38 | { 39 | "selector": "ForInStatement", 40 | "message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array." 41 | }, 42 | { 43 | "selector": "LabeledStatement", 44 | "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." 45 | }, 46 | { 47 | "selector": "WithStatement", 48 | "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." 49 | } 50 | ], 51 | // Overwrites airbnb-base because woks badly with prettier making multyline strings ugly 52 | "prefer-template": "off", 53 | "import/extensions": "off" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set a consistent line-ending styles across different OS to avoid problems with linters 2 | * text eol=lf 3 | *.png binary 4 | *.jpg binary 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @bigcommerce/storefront-team 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | ### Actual behavior 4 | 5 | ### Steps to reproduce behavior 6 | 7 | ### Environment 8 | 9 | Stencil-cli version `stencil --version`: 10 | 11 | Node version `node -v`: 12 | 13 | NPM version `npm -v`: 14 | 15 | OS: 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What? 2 | 3 | A description about what this pull request implements and its purpose. Try to be detailed and describe any technical details to simplify the job of the reviewer and the individual on production support. 4 | 5 | #### Tickets / Documentation 6 | 7 | Add links to any relevant tickets and documentation. 8 | 9 | - [Link 1](http://example.com) 10 | - ... 11 | 12 | #### Screenshots (if appropriate) 13 | 14 | Attach images or add image links here. 15 | 16 | ![Example Image](http://placehold.it/300x200) 17 | 18 | cc @bigcommerce/storefront-team 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_review.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [master, main, release-**] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | node: [18.x, 20.x, 22.x] 12 | os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] 13 | exclude: 14 | - os: windows-latest 15 | node: 22.x 16 | env: 17 | TITLE: ${{ github.event.pull_request.title }} 18 | 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Install python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.10' 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Use Node.js ${{ matrix.node }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node }} 33 | 34 | - name: Install Dependencies 35 | run: npm ci 36 | 37 | - name: Lint the code 38 | run: npm run lint 39 | 40 | - name: Run tests 41 | run: npm run test-with-coverage 42 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_title_check.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Title Check 2 | 3 | on: 4 | pull_request: 5 | branches: [master, main] 6 | 7 | jobs: 8 | check-pr-title: 9 | env: 10 | TITLE: ${{ github.event.pull_request.title }} 11 | 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: '20.x' 18 | cache: 'npm' 19 | - name: Install Dependencies 20 | run: npm ci 21 | 22 | - name: Verify Github PR Title 23 | run: echo $TITLE | npx commitlint 24 | 25 | - name: Verify Git Commit Name 26 | run: git log -1 --pretty=format:"%s" | npx commitlint 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Stencil CLI Release 2 | 3 | on: 4 | push: 5 | branches: [master, main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: '20.x' 15 | - run: npm i 16 | - name: Check Git Commit name 17 | run: git log -1 --pretty=format:"%s" | npx commitlint 18 | # Setup .npmrc file to publish to npm registry 19 | - run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 20 | - name: Deploy to npm and git 21 | run: npm config list && npm run release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | GA_USERNAME: ${{ secrets.PAT_USERNAME }} 25 | GA_TOKEN: ${{ secrets.PAT_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/release_image.yml: -------------------------------------------------------------------------------- 1 | name: Stencil CLI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build-and-push-image: 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | runs-on: ubuntu-latest 13 | permissions: 14 | packages: write 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Log in to the Container registry 21 | uses: docker/login-action@v2 22 | with: 23 | registry: ${{ env.REGISTRY }} 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Extract metadata (tags, labels) for Docker 28 | id: meta 29 | uses: docker/metadata-action@v4 30 | with: 31 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 32 | 33 | - name: Build and push Docker image 34 | uses: docker/build-push-action@v3 35 | with: 36 | context: . 37 | push: ${{ github.event_name != 'pull_request' }} 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode 3 | jspm_packages 4 | node_modules 5 | npm-debug.log 6 | .DS_Store 7 | .coverage/ 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.16 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode 3 | node_modules 4 | .coverage/ 5 | test/_mocks/ 6 | 7 | CHANGELOG.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | "tagFormat": "${version}", 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | "@semantic-release/changelog", 8 | "@semantic-release/github", 9 | "@semantic-release/npm", 10 | [ 11 | "semantic-release-github-pullrequest", { 12 | "assets": ["CHANGELOG.md", "package.json"], 13 | "baseRef": "master" 14 | } 15 | ] 16 | ] 17 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN npm install -g --unsafe-perm @bigcommerce/stencil-cli 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-present, BigCommerce Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by BigCommerce Inc. 14 | 4. Neither the name of BigCommerce Inc. nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY BIGCOMMERCE INC ''AS IS'' AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BIGCOMMERCE INC BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /bin/stencil-bundle.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import program from '../lib/commander.js'; 4 | import { THEME_PATH, PACKAGE_INFO } from '../constants.js'; 5 | import ThemeConfig from '../lib/theme-config.js'; 6 | import Bundle from '../lib/stencil-bundle.js'; 7 | import { printCliResultErrorAndExit, prepareCommand } from '../lib/cliCommon.js'; 8 | import BuildConfigManager from '../lib/BuildConfigManager.js'; 9 | 10 | program 11 | .version(PACKAGE_INFO.version) 12 | .option( 13 | '-d, --dest [dest]', 14 | 'Where to save the zip file. It defaults to the current directory you are in when bundling', 15 | ) 16 | .option('-S, --source-maps', 'Include source-maps in the bundle. This is useful for debugging') 17 | .option( 18 | '-n, --name [filename]', 19 | 'What do you want to call the zip file. It defaults to stencil-bundle.zip', 20 | ) 21 | .option( 22 | '-m, --marketplace', 23 | 'Runs extra bundle validations for partners who can create marketplace themes', 24 | ) 25 | .option( 26 | '-t, --timeout [timeout]', 27 | 'Set a timeout for the bundle operation. Default is 20 secs', 28 | '60', 29 | ); 30 | const cliOptions = prepareCommand(program); 31 | const themeConfig = ThemeConfig.getInstance(THEME_PATH); 32 | async function run() { 33 | try { 34 | if (cliOptions.dest === true) { 35 | throw new Error('You have to specify a value for -d or --dest'.red); 36 | } 37 | if (cliOptions.name === true) { 38 | throw new Error('You have to specify a value for -n or --name'.red); 39 | } 40 | if (!themeConfig.configExists()) { 41 | throw new Error( 42 | `${ 43 | 'You must have a '.red + 'config.json'.cyan 44 | } file in your top level theme directory.`, 45 | ); 46 | } 47 | const rawConfig = await themeConfig.getRawConfig(); 48 | const timeout = cliOptions.timeout * 1000; // seconds 49 | const buildConfigManager = new BuildConfigManager({ timeout }); 50 | const bundle = new Bundle( 51 | THEME_PATH, 52 | themeConfig, 53 | rawConfig, 54 | cliOptions, 55 | buildConfigManager, 56 | ); 57 | const bundlePath = await bundle.initBundle(); 58 | console.log(`Bundled saved to: ${bundlePath.cyan}`); 59 | } catch (err) { 60 | printCliResultErrorAndExit(err); 61 | } 62 | } 63 | run(); 64 | -------------------------------------------------------------------------------- /bin/stencil-debug.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import program from '../lib/commander.js'; 4 | import StencilDebug from '../lib/StencilDebug.js'; 5 | import { PACKAGE_INFO } from '../constants.js'; 6 | import { printCliResultErrorAndExit } from '../lib/cliCommon.js'; 7 | 8 | program 9 | .version(PACKAGE_INFO.version) 10 | .option('-o, --output [filename]', 'If provided will write to file') 11 | .parse(process.argv); 12 | const cliOptions = program.opts(); 13 | new StencilDebug().run(cliOptions).catch(printCliResultErrorAndExit); 14 | -------------------------------------------------------------------------------- /bin/stencil-download.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import inquirer from 'inquirer'; 4 | import program from '../lib/commander.js'; 5 | import { PACKAGE_INFO } from '../constants.js'; 6 | import stencilDownload from '../lib/stencil-download.js'; 7 | import { prepareCommand, printCliResultErrorAndExit } from '../lib/cliCommon.js'; 8 | 9 | program 10 | .version(PACKAGE_INFO.version) 11 | .option('-f, --file [filename]', 'specify the filename to download only') 12 | .option('-e, --exclude [exclude]', 'specify a directory to exclude from download') 13 | .option('-c, --channel_id [channelId]', 'specify the channel ID of the storefront', parseInt) 14 | .option('-o, --overwrite', 'overwrite local with remote files'); 15 | const cliOptions = prepareCommand(program); 16 | const extraExclude = cliOptions.exclude ? [cliOptions.exclude] : []; 17 | const options = { 18 | exclude: ['parsed', 'manifest.json', ...extraExclude], 19 | apiHost: cliOptions.host, 20 | channelId: cliOptions.channel_id, 21 | overwrite: cliOptions.overwrite, 22 | applyTheme: true, 23 | file: cliOptions.file, 24 | }; 25 | async function run(opts) { 26 | const overwriteType = opts.file ? opts.file : 'files'; 27 | const promptAnswers = await inquirer.prompt([ 28 | { 29 | message: `${'Warning'.yellow} -- overwrite local with remote ${overwriteType}?`, 30 | name: 'overwrite', 31 | type: 'confirm', 32 | when() { 33 | return !opts.overwrite; 34 | }, 35 | }, 36 | ]); 37 | const answers = { 38 | ...opts, 39 | ...promptAnswers, 40 | }; 41 | if (!answers.overwrite) { 42 | console.log(`Request cancelled by user ${'No'.red}`); 43 | return; 44 | } 45 | console.log(`${'ok'.green} -- ${overwriteType} will be overwritten by the changes`); 46 | try { 47 | await stencilDownload(opts); 48 | } catch (err) { 49 | printCliResultErrorAndExit(err); 50 | } 51 | console.log(`${'ok'.green} -- Theme file(s) updated from remote`); 52 | } 53 | run(options); 54 | -------------------------------------------------------------------------------- /bin/stencil-init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import program from '../lib/commander.js'; 3 | import StencilInit from '../lib/stencil-init.js'; 4 | import { PACKAGE_INFO } from '../constants.js'; 5 | import { prepareCommand, printCliResultErrorAndExit } from '../lib/cliCommon.js'; 6 | 7 | program 8 | .version(PACKAGE_INFO.version) 9 | .option('-u, --url [url]', 'Store URL') 10 | .option('-t, --token [token]', 'Access Token') 11 | .option('-p, --port [port]', 'Port') 12 | .option('-h, --apiHost [host]', 'API Host') 13 | .option('-pm, --packageManager [pm]', 'Package manager') 14 | .option('-skip, --skipInstall', 'Skip packages installation'); 15 | const cliOptions = prepareCommand(program); 16 | new StencilInit() 17 | .run({ 18 | normalStoreUrl: cliOptions.url, 19 | accessToken: cliOptions.token, 20 | port: cliOptions.port, 21 | apiHost: cliOptions.apiHost, 22 | packageManager: cliOptions.packageManager, 23 | skipInstall: cliOptions.skipInstall, 24 | }) 25 | .catch(printCliResultErrorAndExit); 26 | -------------------------------------------------------------------------------- /bin/stencil-pull.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import { PACKAGE_INFO } from '../constants.js'; 4 | import program from '../lib/commander.js'; 5 | import stencilPull from '../lib/stencil-pull.js'; 6 | import { prepareCommand, printCliResultErrorAndExit } from '../lib/cliCommon.js'; 7 | 8 | program 9 | .version(PACKAGE_INFO.version) 10 | .option('-s, --saved', 'get the saved configuration instead of the active one') 11 | .option( 12 | '-f, --filename [filename]', 13 | 'specify the filename to save the config as', 14 | 'config.json', 15 | ) 16 | .option( 17 | '-c, --channel_id [channelId]', 18 | 'specify the channel ID of the storefront to pull configuration from', 19 | parseInt, 20 | ); 21 | const cliOptions = prepareCommand(program); 22 | const options = { 23 | apiHost: cliOptions.host, 24 | saveConfigName: cliOptions.filename, 25 | channelId: cliOptions.channel_id, 26 | saved: cliOptions.saved || false, 27 | applyTheme: true, // fix to be compatible with stencil-push.utils 28 | }; 29 | stencilPull(options, (err) => { 30 | if (err) { 31 | printCliResultErrorAndExit(err); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /bin/stencil-push.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import { PACKAGE_INFO } from '../constants.js'; 4 | import program from '../lib/commander.js'; 5 | import stencilPush from '../lib/stencil-push.js'; 6 | import { prepareCommand, printCliResultErrorAndExit } from '../lib/cliCommon.js'; 7 | 8 | program 9 | .version(PACKAGE_INFO.version) 10 | .option('-f, --file [filename]', 'specify the filename of the bundle to upload') 11 | .option('-s, --save [filename]', 'specify the filename to save the bundle as') 12 | .option('-S, --source-maps', 'Include source-maps in the bundle. This is useful for debugging') 13 | .option('-a, --activate [variationname]', 'specify the variation of the theme to activate') 14 | .option('-d, --delete', 'delete oldest private theme if upload limit reached') 15 | .option( 16 | '-c, --channel_ids ', 17 | 'specify the channel IDs of the storefront to push the theme to', 18 | ) 19 | .option('-allc, --all_channels', 'push a theme to all available channels'); 20 | const cliOptions = prepareCommand(program); 21 | const options = { 22 | apiHost: cliOptions.host, 23 | channelIds: cliOptions.channel_ids, 24 | bundleZipPath: cliOptions.file, 25 | activate: cliOptions.activate, 26 | saveBundleName: cliOptions.save, 27 | deleteOldest: cliOptions.delete, 28 | allChannels: cliOptions.all_channels, 29 | sourceMaps: cliOptions.source_maps, 30 | }; 31 | stencilPush(options, (err, result) => { 32 | if (err) { 33 | printCliResultErrorAndExit(err); 34 | } 35 | console.log(`${'ok'.green} -- ${result}`); 36 | }); 37 | -------------------------------------------------------------------------------- /bin/stencil-release.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import StencilRelease from '../lib/release/release.js'; 4 | import { PACKAGE_INFO } from '../constants.js'; 5 | import program from '../lib/commander.js'; 6 | import { checkNodeVersion, printCliResultErrorAndExit } from '../lib/cliCommon.js'; 7 | 8 | program 9 | .version(PACKAGE_INFO.version) 10 | .option('-b, --branch [name]', 'specify the main branch name') 11 | .parse(process.argv); 12 | checkNodeVersion(); 13 | const cliOptions = program.opts(); 14 | const options = { 15 | branch: cliOptions.branch || 'master', 16 | }; 17 | new StencilRelease().run(options).catch(printCliResultErrorAndExit); 18 | -------------------------------------------------------------------------------- /bin/stencil-scss-autofix.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import program from '../lib/commander.js'; 4 | import ThemeConfig from '../lib/theme-config.js'; 5 | import NodeSassAutoFixer from '../lib/nodeSass/AutoFixer.js'; 6 | import { THEME_PATH, PACKAGE_INFO } from '../constants.js'; 7 | import { printCliResultErrorAndExit } from '../lib/cliCommon.js'; 8 | 9 | program 10 | .version(PACKAGE_INFO.version) 11 | .option( 12 | '-d, --dry', 13 | 'will not write any changes to the file system, instead it will print the changes to the console', 14 | ) 15 | .parse(process.argv); 16 | const cliOptions = program.opts(); 17 | const themeConfig = ThemeConfig.getInstance(THEME_PATH); 18 | new NodeSassAutoFixer(THEME_PATH, themeConfig, cliOptions).run().catch(printCliResultErrorAndExit); 19 | -------------------------------------------------------------------------------- /bin/stencil-start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'colors'; 3 | import { PACKAGE_INFO } from '../constants.js'; 4 | import program from '../lib/commander.js'; 5 | import StencilStart from '../lib/stencil-start.js'; 6 | import { printCliResultErrorAndExit, prepareCommand } from '../lib/cliCommon.js'; 7 | import BuildConfigManager from '../lib/BuildConfigManager.js'; 8 | 9 | program 10 | .version(PACKAGE_INFO.version) 11 | .option('-o, --open', 'Automatically open default browser') 12 | .option('-v, --variation [name]', 'Set which theme variation to use while developing') 13 | .option('-c, --channelId [channelId]', 'Set the channel id for the storefront') 14 | .option( 15 | '--tunnel [name]', 16 | 'Create a tunnel URL which points to your local server that anyone can use.', 17 | ) 18 | .option( 19 | '-n, --no-cache', 20 | 'Turns off caching for API resource data per storefront page. The cache lasts for 5 minutes before automatically refreshing.', 21 | ) 22 | .option('-t, --timeout', 'Set a timeout for the bundle operation. Default is 20 secs', '60') 23 | .option( 24 | '-cu, --channelUrl [channelUrl]', 25 | 'Set a custom domain url to bypass dns/proxy protection', 26 | ); 27 | const cliOptions = prepareCommand(program); 28 | const options = { 29 | open: cliOptions.open, 30 | variation: cliOptions.variation, 31 | channelId: cliOptions.channelId, 32 | apiHost: cliOptions.host, 33 | tunnel: cliOptions.tunnel, 34 | cache: cliOptions.cache, 35 | channelUrl: cliOptions.channelUrl, 36 | }; 37 | const timeout = cliOptions.timeout * 1000; // seconds 38 | const buildConfigManager = new BuildConfigManager({ timeout }); 39 | new StencilStart({ buildConfigManager }).run(options).catch(printCliResultErrorAndExit); 40 | -------------------------------------------------------------------------------- /bin/stencil.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import program from '../lib/commander.js'; 3 | import { PACKAGE_INFO } from '../constants.js'; 4 | 5 | program 6 | .version(PACKAGE_INFO.version) 7 | .command( 8 | 'init', 9 | 'Interactively create a .stencil file which configures how to run a BigCommerce store locally.', 10 | ) 11 | .command('start', 'Starts up BigCommerce store using theme files in the current directory.') 12 | .command('bundle', 'Bundles up the theme into a zip file which can be uploaded to BigCommerce.') 13 | .command('release', "Create a new release in the theme's github repository.") 14 | .command('push', 'Bundles up the theme into a zip file and uploads it to your store.') 15 | .command('pull', 'Pulls currently active theme config files and overwrites local copy') 16 | .command('download', 'Downloads all the theme files') 17 | .command('debug', 'Prints environment and theme settings for debug purposes') 18 | .command('scss-autofix', 'Prints environment and theme settings for debug purposes') 19 | .parse(process.argv); 20 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [0, 'always', 'sentence-case'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | /// //////////////////////////////////////// Themes /////////////////////////////////////// /// 3 | const THEME_PATH = process.cwd(); 4 | const DEFAULT_CUSTOM_LAYOUTS_CONFIG = { 5 | brand: {}, 6 | category: {}, 7 | page: {}, 8 | product: {}, 9 | }; 10 | /// ///////////////////////////////////////// Other //////////////////////////////////////// /// 11 | const API_HOST = 'https://api.bigcommerce.com'; 12 | 13 | const packageConfigUrl = new URL('./package.json', import.meta.url); 14 | const PACKAGE_INFO = JSON.parse(readFileSync(packageConfigUrl)); 15 | 16 | export { PACKAGE_INFO }; 17 | export { THEME_PATH }; 18 | export { DEFAULT_CUSTOM_LAYOUTS_CONFIG }; 19 | export { API_HOST }; 20 | export default { 21 | PACKAGE_INFO, 22 | THEME_PATH, 23 | DEFAULT_CUSTOM_LAYOUTS_CONFIG, 24 | API_HOST, 25 | }; 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | export default { 5 | collectCoverage: false, 6 | collectCoverageFrom: ['./bin/**/*.js', './server/**/*.js', './lib/**/*.js', './tasks/**/*.js'], 7 | coverageDirectory: './.coverage', 8 | coverageThreshold: { 9 | global: { 10 | branches: 40, 11 | functions: 50, 12 | lines: 50, 13 | statements: 50, 14 | }, 15 | }, 16 | moduleFileExtensions: ['js', 'json', 'node'], 17 | testEnvironment: 'jest-environment-node', 18 | testMatch: ['**/__tests__/**/*.[jt]s', '**/?(*.)+(spec|test).[tj]s'], 19 | // transform: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/BuildConfigManager.js: -------------------------------------------------------------------------------- 1 | import { once } from 'lodash-es'; 2 | import { fork } from 'child_process'; 3 | import path from 'path'; 4 | import fsModule from 'fs'; 5 | import { createRequire } from 'node:module'; 6 | import { THEME_PATH } from '../constants.js'; 7 | 8 | const require = createRequire(import.meta.url); 9 | 10 | class BuildConfigManager { 11 | constructor({ workDir = THEME_PATH, fs = fsModule, timeout = 20000 } = {}) { 12 | this.oldConfigFileName = 'stencil.conf.js'; 13 | this.configFileName = 'stencil.conf.cjs'; 14 | this._workDir = workDir; 15 | this._buildConfigPath = path.join(workDir, this.configFileName); 16 | this._oldBuildConfigPath = path.join(workDir, this.oldConfigFileName); 17 | this._fs = fs; 18 | this._onReadyCallbacks = []; 19 | this._worker = null; 20 | this._workerIsReady = false; 21 | this.timeout = timeout; 22 | const config = this._getConfig(this._buildConfigPath, this._oldBuildConfigPath); 23 | this.development = config.development || this._devWorker; 24 | this.production = config.production || this._prodWorker; 25 | this.watchOptions = config.watchOptions; 26 | } 27 | 28 | initWorker() { 29 | if (this._fs.existsSync(this._buildConfigPath)) { 30 | this._worker = fork(this._buildConfigPath, [], { cwd: this._workDir }); 31 | this._worker.on('message', (message) => { 32 | if (message === 'ready') { 33 | this._workerIsReady = true; 34 | this._onReadyCallbacks.forEach((callback) => callback()); 35 | } 36 | }); 37 | } 38 | return this; 39 | } 40 | 41 | stopWorker(signal = 'SIGTERM') { 42 | this._worker.kill(signal); 43 | } 44 | 45 | _getConfig(configPath, oldConfigPath) { 46 | if (this._fs.existsSync(configPath)) { 47 | // eslint-disable-next-line import/no-dynamic-require 48 | return require(configPath); 49 | } 50 | if (this._fs.existsSync(oldConfigPath)) { 51 | this._moveOldConfig(configPath, oldConfigPath); 52 | // eslint-disable-next-line import/no-dynamic-require 53 | return require(configPath); 54 | } 55 | return {}; 56 | } 57 | 58 | _moveOldConfig(newPath, oldPath) { 59 | this._fs.copyFileSync(oldPath, newPath); 60 | this._fs.rmSync(oldPath); 61 | } 62 | 63 | _onWorkerReady(onReady) { 64 | if (this._workerIsReady) { 65 | process.nextTick(onReady); 66 | } 67 | this._onReadyCallbacks.push(onReady); 68 | } 69 | 70 | _devWorker(browserSync) { 71 | if (!this._worker) { 72 | return; 73 | } 74 | // send a message to the worker to start watching 75 | // and wait for message to reload the browser 76 | this._worker.send('development'); 77 | this._worker.on('message', (message) => { 78 | if (message === 'reload') { 79 | browserSync.reload(); 80 | } 81 | }); 82 | } 83 | 84 | _prodWorker(done) { 85 | const callback = once(done); 86 | if (!this._worker) { 87 | process.nextTick(() => callback('worker initialization failed')); 88 | return; 89 | } 90 | const timeoutId = setTimeout(() => { 91 | this.stopWorker(); 92 | console.log( 93 | 'The process was timed out. Try to increase it by providing --timeout [number] option' 94 | .yellow, 95 | ); 96 | callback('worker timed out'); 97 | }, this.timeout); 98 | this._onWorkerReady(() => { 99 | clearTimeout(timeoutId); 100 | // send a message to the worker to start bundling js 101 | this._worker.send('production'); 102 | this._worker.on('message', (message) => { 103 | if (message === 'done') { 104 | this._worker.kill(); 105 | callback(); 106 | } 107 | }); 108 | }); 109 | this._worker.on('close', () => { 110 | callback('worker terminated'); 111 | clearTimeout(timeoutId); 112 | }); 113 | } 114 | } 115 | export default BuildConfigManager; 116 | -------------------------------------------------------------------------------- /lib/BuildConfigManager.spec.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import { promisify } from 'util'; 3 | import BuildConfigManager from './BuildConfigManager.js'; 4 | 5 | const cwd = process.cwd(); 6 | describe('BuildConfigManager integration tests', () => { 7 | afterEach(() => { 8 | jest.restoreAllMocks(); 9 | }); 10 | describe('constructor', () => { 11 | it('should return an instance with correct watchOptions taken from the config file', () => { 12 | const buildConfig = new BuildConfigManager({ 13 | workDir: `${cwd}/test/_mocks/build-config/valid-config`, 14 | }); 15 | expect(buildConfig.watchOptions).toBeInstanceOf(Object); 16 | expect(buildConfig.watchOptions.files).toBeInstanceOf(Array); 17 | expect(buildConfig.watchOptions.ignored).toBeInstanceOf(Array); 18 | }); 19 | }); 20 | describe('production method', () => { 21 | it('should resolve successfully for "valid-config"', async () => { 22 | const buildConfig = new BuildConfigManager({ 23 | workDir: `${cwd}/test/_mocks/build-config/valid-config`, 24 | }); 25 | buildConfig.initWorker(); 26 | expect(buildConfig.production).toBeInstanceOf(Function); 27 | await promisify(buildConfig.production.bind(buildConfig))(); 28 | buildConfig.stopWorker(); 29 | }); 30 | it('should resolve successfully for "legacy-config"', async () => { 31 | const buildConfig = new BuildConfigManager({ 32 | workDir: `${cwd}/test/_mocks/build-config/legacy-config`, 33 | }); 34 | buildConfig.initWorker(); 35 | expect(buildConfig.production).toBeInstanceOf(Function); 36 | await promisify(buildConfig.production.bind(buildConfig))(); 37 | buildConfig.stopWorker(); 38 | }); 39 | it('should reject with "worker terminated" message for "noworker-config"', async () => { 40 | const buildConfig = new BuildConfigManager({ 41 | workDir: `${cwd}/test/_mocks/build-config/noworker-config`, 42 | }); 43 | const initedBuildConfig = buildConfig.initWorker(); 44 | expect(buildConfig.production).toBeInstanceOf(Function); 45 | await expect( 46 | promisify(initedBuildConfig.production.bind(initedBuildConfig))(), 47 | ).rejects.toContain('worker terminated'); 48 | buildConfig.stopWorker(); 49 | }); 50 | }); 51 | describe('development method', () => { 52 | it('should reload the browser when a message "reload" is received from stencil.conf.js (valid-config)', async () => { 53 | const buildConfig = new BuildConfigManager({ 54 | workDir: `${cwd}/test/_mocks/build-config/valid-config`, 55 | }); 56 | expect(buildConfig.development).toBeInstanceOf(Function); 57 | await new Promise((done) => buildConfig.initWorker().development({ reload: done })); 58 | buildConfig.stopWorker(); 59 | }); 60 | it('should reload the browser when "reload" method is called from stencil.conf.js (legacy-config)', async () => { 61 | const buildConfig = new BuildConfigManager({ 62 | workDir: `${cwd}/test/_mocks/build-config/legacy-config`, 63 | }); 64 | expect(buildConfig.development).toBeInstanceOf(Function); 65 | await new Promise((done) => buildConfig.initWorker().development({ reload: done })); 66 | buildConfig.stopWorker(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /lib/Cycles.js: -------------------------------------------------------------------------------- 1 | import Graph from 'tarjan-graph'; 2 | import util from 'util'; 3 | 4 | class Cycles { 5 | /** 6 | * @param {object[]} templatePaths 7 | */ 8 | constructor(templatePaths) { 9 | if (!Array.isArray(templatePaths)) { 10 | throw new Error('templatePaths must be an Array'); 11 | } 12 | this.templatePaths = templatePaths; 13 | this.partialRegex = /{{>\s*([_|\-|a-zA-Z0-9/]+)[^{]*?}}/g; 14 | this.dynamicComponentRegex = /{{\s*?dynamicComponent\s*(?:'|")([_|\-|a-zA-Z0-9/]+)(?:'|").*?}}/g; 15 | } 16 | 17 | /** 18 | * Runs a graph based cyclical dependency check. Throws an error if circular dependencies are found 19 | * @returns {void} 20 | */ 21 | detect() { 22 | for (const templatesByPath of this.templatePaths) { 23 | const graph = new Graph(); 24 | for (const [templatePath, templateContent] of Object.entries(templatesByPath)) { 25 | const dependencies = [ 26 | ...this._geDependantPartials(templateContent, templatePath), 27 | ...this._getDependantDynamicComponents( 28 | templateContent, 29 | templatesByPath, 30 | templatePath, 31 | ), 32 | ]; 33 | graph.add(templatePath, dependencies); 34 | } 35 | if (graph.hasCycle()) { 36 | const foundCycles = util.inspect(graph.getCycles()); 37 | throw new Error(`Circular dependency in template detected. \r\n${foundCycles}`); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * @private 44 | * @param {string} templateContent 45 | * @param {string} pathToSkip 46 | * @returns {string[]} 47 | */ 48 | _geDependantPartials(templateContent, pathToSkip) { 49 | const dependencies = []; 50 | let match = this.partialRegex.exec(templateContent); 51 | while (match !== null) { 52 | const partialPath = match[1]; 53 | // skip the current templatePath 54 | if (partialPath !== pathToSkip) { 55 | dependencies.push(partialPath); 56 | } 57 | match = this.partialRegex.exec(templateContent); 58 | } 59 | return dependencies; 60 | } 61 | 62 | /** 63 | * @private 64 | * @param {string} templateContent 65 | * @param {object} allTemplatesByPath 66 | * @param {string} pathToSkip 67 | * @returns {string[]} 68 | */ 69 | _getDependantDynamicComponents(templateContent, allTemplatesByPath, pathToSkip) { 70 | const dependencies = []; 71 | let match = this.dynamicComponentRegex.exec(templateContent); 72 | while (match !== null) { 73 | const dynamicComponents = this._getDynamicComponents( 74 | match[1], 75 | allTemplatesByPath, 76 | pathToSkip, 77 | ); 78 | dependencies.push(...dynamicComponents); 79 | match = this.dynamicComponentRegex.exec(templateContent); 80 | } 81 | return dependencies; 82 | } 83 | 84 | /** 85 | * @private 86 | * @param {string} componentFolder 87 | * @param {object} possibleTemplates 88 | * @param {string} pathToSkip 89 | * @returns {string[]} 90 | */ 91 | _getDynamicComponents(componentFolder, possibleTemplates, pathToSkip) { 92 | return Object.keys(possibleTemplates).reduce((output, templatePath) => { 93 | if (templatePath.indexOf(componentFolder) === 0 && templatePath !== pathToSkip) { 94 | output.push(templatePath); 95 | } 96 | return output; 97 | }, []); 98 | } 99 | } 100 | export default Cycles; 101 | -------------------------------------------------------------------------------- /lib/ScssValidator.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import path from 'path'; 3 | import StencilStyles from '@bigcommerce/stencil-styles'; 4 | import cssCompiler from './css/compile.js'; 5 | 6 | class ScssValidator { 7 | /** 8 | * 9 | * @param themePath 10 | * @param themeConfig 11 | */ 12 | constructor(themePath, themeConfig) { 13 | this.themePath = themePath; 14 | this.themeConfig = themeConfig; 15 | } 16 | 17 | async run() { 18 | const assetsPath = path.join(this.themePath, 'assets'); 19 | const stylesPath = path.join(this.themePath, 'assets/scss'); 20 | const rawConfig = await this.themeConfig.getConfig(); 21 | const cssFiles = await this.getCssFiles(); 22 | for await (const file of cssFiles) { 23 | try { 24 | /* eslint-disable-next-line no-await-in-loop */ 25 | await cssCompiler.compile( 26 | rawConfig, 27 | assetsPath, 28 | file, 29 | cssCompiler.SASS_ENGINE_NAME, 30 | ); 31 | } catch (e) { 32 | const message = this.parseStencilStylesError(e); 33 | throw new Error( 34 | `${message} while compiling css files from "${stylesPath}/${file}".`.red, 35 | ); 36 | } 37 | } 38 | } 39 | 40 | parseStencilStylesError(e) { 41 | if (e.formatted) { 42 | return `${e.formatted.replace('Error: ', '')}: `; 43 | } 44 | return e.message; 45 | } 46 | 47 | getCssFiles() { 48 | const styles = new StencilStyles(); 49 | return styles.getCssFiles(this.themePath); 50 | } 51 | } 52 | export default ScssValidator; 53 | -------------------------------------------------------------------------------- /lib/ScssValidator.spec.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import ScssValidator from './ScssValidator.js'; 3 | import ThemeConfig from './theme-config.js'; 4 | 5 | describe('ScssValidator integration tests', () => { 6 | it('should throw an error when scss fail has conditional import issue on latest node sass', async () => { 7 | const themePath = path.join( 8 | process.cwd(), 9 | 'test/_mocks/themes/invalid-scss-latest-node-sass', 10 | ); 11 | const themeConfig = ThemeConfig.getInstance(themePath); 12 | const validator = new ScssValidator(themePath, themeConfig); 13 | await expect(validator.run()).rejects.toThrow( 14 | 'Import directives may not be used within control directives or mixins.', 15 | ); 16 | }); 17 | it('should successfully compile on latest node sass', async () => { 18 | const themePath = path.join(process.cwd(), 'test/_mocks/themes/valid'); 19 | const themeConfig = ThemeConfig.getInstance(themePath); 20 | const validator = new ScssValidator(themePath, themeConfig); 21 | await expect(validator.run()).resolves.not.toThrow(Error); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /lib/StencilCLISettings.js: -------------------------------------------------------------------------------- 1 | class StencilCLISettings { 2 | constructor() { 3 | // The setting enables/disables verbose network requests logging 4 | this.versboseNetworkLogging = false; 5 | // Enables usage of node sass fork for testing purposes 6 | this.oldNodeSassFork = false; 7 | } 8 | 9 | setVerbose(flag) { 10 | this.versboseNetworkLogging = flag; 11 | } 12 | 13 | isVerbose() { 14 | return this.versboseNetworkLogging; 15 | } 16 | } 17 | const settings = new StencilCLISettings(); 18 | export default settings; 19 | -------------------------------------------------------------------------------- /lib/StencilCLISettings.spec.js: -------------------------------------------------------------------------------- 1 | import stencilCLISettings from './StencilCLISettings.js'; 2 | 3 | describe('StencilCLISettings', () => { 4 | afterEach(() => { 5 | // setting to default 6 | stencilCLISettings.setVerbose(false); 7 | }); 8 | it('should set network logging to non-verbose by default', () => { 9 | expect(stencilCLISettings.isVerbose()).toBeFalsy(); 10 | }); 11 | it('should set network logging to verbose', () => { 12 | stencilCLISettings.setVerbose(true); 13 | expect(stencilCLISettings.isVerbose()).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /lib/StencilDebug.js: -------------------------------------------------------------------------------- 1 | import fsModule from 'fs'; 2 | import path from 'path'; 3 | import osModule from 'os'; 4 | import { PACKAGE_INFO, THEME_PATH } from '../constants.js'; 5 | import ThemeConfig from './theme-config.js'; 6 | import StencilConfigManager from './StencilConfigManager.js'; 7 | 8 | class StencilDebug { 9 | constructor({ 10 | fs = fsModule, 11 | os = osModule, 12 | logger = console, 13 | themeConfig = ThemeConfig.getInstance(THEME_PATH), 14 | stencilConfigManager = new StencilConfigManager(), 15 | } = {}) { 16 | this._fs = fs; 17 | this._os = os; 18 | this._logger = logger; 19 | this._themeConfig = themeConfig; 20 | this._stencilConfigManager = stencilConfigManager; 21 | } 22 | 23 | async run(options) { 24 | const platform = this.getPlatformInfo(); 25 | const nodeVersion = this.getNodeJsVersion(); 26 | const version = this.getCliVersion(); 27 | const theme = await this.getThemeInfo(); 28 | const stencil = await this.getStencilInfo(); 29 | const info = { 30 | platform, 31 | version, 32 | nodeVersion, 33 | stencil, 34 | theme, 35 | }; 36 | const result = this.prepareResult(info); 37 | await this.printResult(result, options); 38 | } 39 | 40 | getPlatformInfo() { 41 | return { 42 | type: this._os.type(), 43 | version: this._os.version(), 44 | }; 45 | } 46 | 47 | getNodeJsVersion() { 48 | return process.version; 49 | } 50 | 51 | getCliVersion() { 52 | return PACKAGE_INFO.version; 53 | } 54 | 55 | async getThemeInfo() { 56 | this.checkExecutableLocation(this._themeConfig); 57 | const rawConfig = await this._themeConfig.getRawConfig(); 58 | const { 59 | name, 60 | version, 61 | /* eslint-disable camelcase */ 62 | template_engine, 63 | css_compiler, 64 | meta: { author_name }, 65 | /* eslint-enable camelcase */ 66 | } = rawConfig; 67 | return { 68 | name, 69 | version, 70 | template_engine, 71 | css_compiler, 72 | author_name, 73 | }; 74 | } 75 | 76 | async getStencilInfo() { 77 | const { apiHost, normalStoreUrl, port } = await this._stencilConfigManager.read(); 78 | return { 79 | apiHost, 80 | normalStoreUrl, 81 | port, 82 | }; 83 | } 84 | 85 | checkExecutableLocation(themeConfig) { 86 | if (!themeConfig.configExists()) { 87 | throw new Error( 88 | `${ 89 | 'You must have a '.red + 'config.json'.cyan 90 | } file in your top level theme directory.`, 91 | ); 92 | } 93 | } 94 | 95 | prepareResult(data) { 96 | return JSON.stringify(data); 97 | } 98 | 99 | async printResult(result, options) { 100 | if (options.output) { 101 | const filePath = options.output.startsWith('/') 102 | ? options.output 103 | : path.join(process.cwd(), options.output); 104 | await this._fs.promises.writeFile(filePath, result); 105 | } else { 106 | this._logger.log(result); 107 | } 108 | } 109 | } 110 | export default StencilDebug; 111 | -------------------------------------------------------------------------------- /lib/StencilDebug.spec.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import StencilDebug from './StencilDebug.js'; 3 | import { PACKAGE_INFO } from '../constants.js'; 4 | 5 | describe('StencilDebug', () => { 6 | const name = 'Cornerstone'; 7 | const version = '6.3.0'; 8 | /* eslint-disable-next-line camelcase */ 9 | const template_engine = 'handlebars_v4'; 10 | /* eslint-disable-next-line camelcase */ 11 | const css_compiler = 'scss'; 12 | /* eslint-disable-next-line camelcase */ 13 | const author_name = 'Bigcommerce'; 14 | const port = 3000; 15 | const apiHost = 'https://api.bigcommerce.com'; 16 | const normalStoreUrl = 'https://shop.bigcommerce.com'; 17 | const osType = 'Darwin'; 18 | const osVersion = 19 | 'Darwin Kernel Version 21.3.0: Wed Jan 5 21:37:58 PST 2022; root:xnu-8019.80.24~20/RELEASE_X86_64'; 20 | let logger; 21 | let themeConfig; 22 | let stencilConfigManager; 23 | let os; 24 | beforeEach(() => { 25 | logger = { 26 | log: jest.fn(), 27 | }; 28 | themeConfig = { 29 | configExists: () => true, 30 | getRawConfig: () => ({ 31 | name, 32 | version, 33 | template_engine, 34 | css_compiler, 35 | meta: { 36 | author_name, 37 | }, 38 | }), 39 | }; 40 | stencilConfigManager = { 41 | read: () => ({ 42 | port, 43 | apiHost, 44 | normalStoreUrl, 45 | }), 46 | }; 47 | os = { 48 | type: () => osType, 49 | version: () => osVersion, 50 | }; 51 | }); 52 | afterEach(() => { 53 | jest.restoreAllMocks(); 54 | }); 55 | it('should return stencil debug information', async () => { 56 | await new StencilDebug({ logger, themeConfig, stencilConfigManager, os }).run({ 57 | output: false, 58 | }); 59 | const result = { 60 | platform: { 61 | type: osType, 62 | version: osVersion, 63 | }, 64 | version: PACKAGE_INFO.version, 65 | nodeVersion: process.version, 66 | stencil: { 67 | apiHost, 68 | normalStoreUrl, 69 | port, 70 | }, 71 | theme: { 72 | name, 73 | version, 74 | template_engine, 75 | css_compiler, 76 | author_name, 77 | }, 78 | }; 79 | expect(logger.log).toHaveBeenCalledWith(JSON.stringify(result)); 80 | }); 81 | it('should throw an error, when command is run outside theme location', async () => { 82 | await expect(new StencilDebug().run()).rejects.toThrow(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /lib/archiveManager.js: -------------------------------------------------------------------------------- 1 | import yauzl from 'yauzl'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { promisify } from 'util'; 5 | import stream from 'stream'; 6 | 7 | const pipeline = promisify(stream.pipeline); 8 | /** 9 | * @param {object} options 10 | * @param {string} options.zipPath 11 | * @param {string} [options.fileToExtract] - filename to extract only 12 | * @param {string[]} [options.exclude] - paths of files and directories to exclude 13 | * @param {object} [options.outputNames] - new names for some files. Format: { 'oldName1': 'newName1', ...} 14 | * @returns {Promise} 15 | */ 16 | async function extractZipFiles({ zipPath, fileToExtract, exclude = [], outputNames = {} }) { 17 | let foundMatch = false; 18 | const zipFile = await promisify(yauzl.open)(zipPath, { lazyEntries: true }); 19 | await new Promise((resolve, reject) => { 20 | zipFile.on('entry', async (entry) => { 21 | try { 22 | foundMatch = fileToExtract === entry.fileName; 23 | const isNotTheSearchedFile = fileToExtract && !foundMatch; 24 | const isDirectory = /\/$/.test(entry.fileName); 25 | const isExcluded = 26 | exclude.length && 27 | // startsWith used to exclude files from the specified directories 28 | exclude.some((excludeItem) => entry.fileName.startsWith(excludeItem)); 29 | if (isDirectory || isNotTheSearchedFile || isExcluded) { 30 | zipFile.readEntry(); 31 | return; 32 | } 33 | const outputFilePath = outputNames[entry.fileName] || entry.fileName; 34 | const outputDir = path.parse(outputFilePath).dir; 35 | // Create a directory if the parent directory does not exists 36 | if (outputDir && !fs.existsSync(outputDir)) { 37 | await fs.promises.mkdir(outputDir, { recursive: true }); 38 | } 39 | const readableStream = await promisify(zipFile.openReadStream.bind(zipFile))(entry); 40 | const writeStream = fs.createWriteStream(outputFilePath, { flag: 'w+' }); 41 | await pipeline(readableStream, writeStream); 42 | if (foundMatch) { 43 | zipFile.close(); 44 | resolve(); 45 | return; 46 | } 47 | zipFile.readEntry(); 48 | } catch (err) { 49 | reject(err); 50 | } 51 | }); 52 | zipFile.readEntry(); 53 | zipFile.once('end', () => { 54 | zipFile.close(); 55 | if (!foundMatch && fileToExtract) { 56 | reject(new Error(`${fileToExtract} not found`)); 57 | return; 58 | } 59 | resolve(); 60 | }); 61 | }); 62 | } 63 | export { extractZipFiles }; 64 | export default { 65 | extractZipFiles, 66 | }; 67 | -------------------------------------------------------------------------------- /lib/cliCommon.js: -------------------------------------------------------------------------------- 1 | import semver from 'semver'; 2 | import { PACKAGE_INFO } from '../constants.js'; 3 | import stencilCLISettings from './StencilCLISettings.js'; 4 | 5 | const messages = { 6 | visitTroubleshootingPage: 7 | 'Please visit the troubleshooting page https://developer.bigcommerce.com/stencil-docs/deploying-a-theme/troubleshooting-theme-uploads.', 8 | submitGithubIssue: 9 | 'If this error persists, please visit https://github.com/bigcommerce/stencil-cli/issues and submit an issue.', 10 | }; 11 | /** 12 | * @param {Object} object 13 | 14 | * @returns {void} 15 | */ 16 | function printObject(object) { 17 | for (const property of Object.keys(object)) { 18 | console.log(`${property}: ${object[property]}`); 19 | } 20 | } 21 | /** 22 | * @param {Error} error 23 | 24 | * @returns {void} 25 | */ 26 | function printNetworkError(config) { 27 | console.log(config); 28 | console.log(`URL: `.yellow + config.url); 29 | console.log(`Method: `.yellow + config.method.toUpperCase()); 30 | if (config.data) { 31 | console.log(`Data: `.yellow); 32 | printObject(config.data); 33 | } 34 | } 35 | /** 36 | * @param {Error} error 37 | * @param {Array<{message: string}>} [error.messages] 38 | * @returns {void} 39 | */ 40 | function printCliResultError(error) { 41 | console.error(`\n\n${'not ok'.red} -- ${error || 'Unknown error'}\n`); 42 | if (error && Array.isArray(error.messages)) { 43 | for (const item of error.messages) { 44 | if (item && item.message) { 45 | console.log(`${item.message.red}\n`); 46 | } 47 | } 48 | } 49 | if (error && (error.config || error.response)) { 50 | // In case if request didn't receive any response, response object is not available 51 | const networkReq = error.config || error.response; 52 | printNetworkError(networkReq); 53 | } 54 | console.log(messages.visitTroubleshootingPage); 55 | console.log(messages.submitGithubIssue); 56 | } 57 | /** 58 | * @param {Error} error 59 | * @returns {void} 60 | */ 61 | function printCliResultErrorAndExit(error) { 62 | console.log(error); 63 | printCliResultError(error); 64 | // Exit with error code so automated systems recognize it as a failure 65 | // eslint-disable-next-line no-process-exit 66 | process.exit(1); 67 | } 68 | function checkNodeVersion() { 69 | const satisfies = semver.satisfies(process.versions.node, PACKAGE_INFO.engines.node); 70 | if (!satisfies) { 71 | throw new Error( 72 | `You are running an unsupported version of node. Please upgrade to ${PACKAGE_INFO.engines.node}`, 73 | ); 74 | } 75 | return satisfies; 76 | } 77 | function applyCommonOptions(program) { 78 | program 79 | .option('-h, --host [hostname]', 'specify the api host') 80 | .option('-vb, --verbose', 'enable verbose info logging', false) 81 | .option( 82 | '--use-old-node-sass-fork', 83 | 'use old node sass fork for scss compilation during bundling', 84 | false, 85 | ) 86 | .parse(process.argv); 87 | } 88 | function setupCLI(options) { 89 | stencilCLISettings.setVerbose(options.verbose); 90 | } 91 | function prepareCommand(program) { 92 | applyCommonOptions(program); 93 | checkNodeVersion(); 94 | const options = program.opts(); 95 | setupCLI(options); 96 | return options; 97 | } 98 | export { messages }; 99 | export { printCliResultError }; 100 | export { printCliResultErrorAndExit }; 101 | export { checkNodeVersion }; 102 | export { prepareCommand }; 103 | export default { 104 | messages, 105 | printCliResultError, 106 | printCliResultErrorAndExit, 107 | checkNodeVersion, 108 | prepareCommand, 109 | }; 110 | -------------------------------------------------------------------------------- /lib/cliCommon.spec.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import { jest } from '@jest/globals'; 3 | import { printCliResultError, messages } from './cliCommon.js'; 4 | 5 | describe('cliCommon', () => { 6 | describe('printCliResultError', () => { 7 | let consoleLogStub; 8 | let consoleErrorStub; 9 | beforeAll(() => { 10 | consoleLogStub = jest.spyOn(console, 'log').mockImplementation(jest.fn()); 11 | consoleErrorStub = jest.spyOn(console, 'error').mockImplementation(jest.fn()); 12 | }); 13 | afterEach(() => { 14 | jest.clearAllMocks(); 15 | }); 16 | afterAll(() => { 17 | jest.restoreAllMocks(); 18 | }); 19 | it('should log "Unknown error" and general recommendations if input is empty', () => { 20 | printCliResultError(null); 21 | expect(consoleLogStub).toHaveBeenCalledTimes(2); 22 | expect(consoleErrorStub).toHaveBeenCalledWith(expect.stringMatching('Unknown error')); 23 | expect(consoleLogStub).toHaveBeenCalledWith(messages.visitTroubleshootingPage); 24 | expect(consoleLogStub).toHaveBeenCalledWith(messages.submitGithubIssue); 25 | }); 26 | it('should log the passed error and general recommendations if input is a plain Error object with no extra messages', () => { 27 | const err = new Error('test error'); 28 | printCliResultError(err); 29 | expect(consoleLogStub).toHaveBeenCalledTimes(2); 30 | expect(consoleErrorStub).toHaveBeenCalledWith(expect.stringMatching(err.toString())); 31 | expect(consoleLogStub).toHaveBeenCalledWith(messages.visitTroubleshootingPage); 32 | expect(consoleLogStub).toHaveBeenCalledWith(messages.submitGithubIssue); 33 | }); 34 | it('should log the passed message and general recommendations if input is a string', () => { 35 | const errStr = 'test error message'; 36 | printCliResultError(errStr); 37 | expect(consoleLogStub).toHaveBeenCalledTimes(2); 38 | expect(consoleErrorStub).toHaveBeenCalledWith(expect.stringMatching(errStr)); 39 | expect(consoleLogStub).toHaveBeenCalledWith(messages.visitTroubleshootingPage); 40 | expect(consoleLogStub).toHaveBeenCalledWith(messages.submitGithubIssue); 41 | }); 42 | it('should log the error, each field in error.messages and general recommendations if input is an object with error.messages field', () => { 43 | const err = new Error('test error'); 44 | err.messages = [{ message: 'first_error' }, { message: '2nd_error' }]; 45 | printCliResultError(err); 46 | expect(consoleLogStub).toHaveBeenCalledTimes(4); 47 | expect(consoleErrorStub).toHaveBeenCalledWith(expect.stringMatching(err.toString())); 48 | expect(consoleLogStub).toHaveBeenCalledWith(`${err.messages[0].message.red}\n`); 49 | expect(consoleLogStub).toHaveBeenCalledWith(`${err.messages[1].message.red}\n`); 50 | expect(consoleLogStub).toHaveBeenCalledWith(messages.visitTroubleshootingPage); 51 | expect(consoleLogStub).toHaveBeenCalledWith(messages.submitGithubIssue); 52 | }); 53 | it('should skip non object elements in the error.message array', () => { 54 | const err = new Error('test error'); 55 | err.messages = [ 56 | { message: 'first_error' }, 57 | 'string', 58 | { message: '2nd_error' }, 59 | undefined, 60 | null, 61 | 228, 62 | true, 63 | ]; 64 | printCliResultError(err); 65 | expect(consoleLogStub).toHaveBeenCalledTimes(4); 66 | expect(consoleErrorStub).toHaveBeenCalledWith(expect.stringMatching(err.toString())); 67 | expect(consoleLogStub).toHaveBeenCalledWith(`${'first_error'.red}\n`); 68 | expect(consoleLogStub).toHaveBeenCalledWith(`${'2nd_error'.red}\n`); 69 | expect(consoleLogStub).toHaveBeenCalledWith(messages.visitTroubleshootingPage); 70 | expect(consoleLogStub).toHaveBeenCalledWith(messages.submitGithubIssue); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /lib/commander.js: -------------------------------------------------------------------------------- 1 | import program from 'commander'; 2 | 3 | program 4 | // Avoid option name clashes https://www.npmjs.com/package/commander#avoiding-option-name-clashes 5 | .storeOptionsAsProperties(false) 6 | .passCommandToAction(false); 7 | export default program; 8 | -------------------------------------------------------------------------------- /lib/content-api-client.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import NetworkUtils from './utils/NetworkUtils.js'; 3 | import { 4 | renderedRegionsByPageTypeQuery, 5 | renderedRegionsByPageTypeAndEntityIdQuery, 6 | } from './graphql/query.js'; 7 | 8 | const networkUtils = new NetworkUtils(); 9 | /** 10 | * @param {object} options 11 | * @param {string} options.accessToken 12 | * @param {string} options.storeUrl 13 | * @param {string} pageType 14 | * @returns {Promise} 15 | */ 16 | async function getRenderedRegionsByPageType({ accessToken, storeUrl, pageType }) { 17 | try { 18 | const query = renderedRegionsByPageTypeQuery(pageType); 19 | const response = await networkUtils.sendApiRequest({ 20 | url: `${storeUrl}/graphql`, 21 | headers: { 22 | 'cache-control': 'no-cache', 23 | 'content-type': 'application/json', 24 | Authorization: `Bearer ${accessToken}`, 25 | }, 26 | method: 'POST', 27 | data: JSON.stringify({ 28 | query, 29 | }), 30 | }); 31 | if (!response.data.data) { 32 | return { renderedRegions: [] }; 33 | } 34 | const { 35 | site: { 36 | content: { 37 | renderedRegionsByPageType: { regions }, 38 | }, 39 | }, 40 | } = response.data.data; 41 | return { renderedRegions: regions }; 42 | } catch (err) { 43 | throw new Error(`Could not fetch the rendered regions for this page type: ${err.message}`); 44 | } 45 | } 46 | /** 47 | * @param {object} options 48 | * @param {string} options.accessToken 49 | * @param {string} options.storeUrl 50 | * @param {string} pageType 51 | * @param {number} entityId 52 | * @returns {Promise} 53 | */ 54 | async function getRenderedRegionsByPageTypeAndEntityId({ 55 | accessToken, 56 | storeUrl, 57 | pageType, 58 | entityId, 59 | }) { 60 | try { 61 | const query = renderedRegionsByPageTypeAndEntityIdQuery(pageType, entityId); 62 | const response = await networkUtils.sendApiRequest({ 63 | url: `${storeUrl}/graphql`, 64 | headers: { 65 | 'cache-control': 'no-cache', 66 | 'content-type': 'application/json', 67 | Authorization: `Bearer ${accessToken}`, 68 | }, 69 | method: 'POST', 70 | data: JSON.stringify({ 71 | query, 72 | }), 73 | }); 74 | if (!response.data.data) { 75 | return { renderedRegions: [] }; 76 | } 77 | const { 78 | site: { 79 | content: { 80 | renderedRegionsByPageTypeAndEntityId: { regions }, 81 | }, 82 | }, 83 | } = response.data.data; 84 | return { 85 | renderedRegions: regions, 86 | }; 87 | } catch (err) { 88 | throw new Error(`Could not fetch the rendered regions for this page type: ${err.message}`); 89 | } 90 | } 91 | 92 | export default { 93 | getRenderedRegionsByPageType, 94 | getRenderedRegionsByPageTypeAndEntityId, 95 | }; 96 | -------------------------------------------------------------------------------- /lib/css/compile.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import StencilStyles from '@bigcommerce/stencil-styles'; 3 | 4 | const SASS_ENGINE_NAME = 'node-sass'; 5 | const compile = async (configuration, themeAssetsPath, fileName, engineName = SASS_ENGINE_NAME) => { 6 | const fileParts = path.parse(fileName); 7 | const ext = configuration.css_compiler === 'css' ? configuration.css_compiler : 'scss'; 8 | const pathToFile = path.join(fileParts.dir, `${fileParts.name}.${ext}`); 9 | const basePath = path.join(themeAssetsPath, `${ext}`); 10 | const stencilStyles = new StencilStyles(console); 11 | 12 | let files; 13 | try { 14 | files = await stencilStyles.assembleCssFiles(pathToFile, basePath, `${ext}`, {}); 15 | } catch (err) { 16 | console.error(err); 17 | throw err; 18 | } 19 | const params = { 20 | data: files[pathToFile], 21 | files, 22 | dest: path.join('/assets/css', fileName), 23 | themeSettings: configuration.settings, 24 | sourceMap: true, 25 | autoprefixerOptions: { 26 | cascade: configuration.autoprefixer_cascade, 27 | overrideBrowserslist: configuration.autoprefixer_browsers, 28 | }, 29 | }; 30 | stencilStyles.activateEngine(engineName); 31 | return stencilStyles.compileCss('scss', params); 32 | }; 33 | 34 | export default { 35 | compile, 36 | SASS_ENGINE_NAME, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/graphql/query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} pageType 3 | * @returns {string}>} 4 | */ 5 | const renderedRegionsByPageTypeQuery = (pageType) => `query { 6 | site { 7 | content { 8 | renderedRegionsByPageType(pageType: ${pageType}) { 9 | regions { 10 | name 11 | html 12 | } 13 | } 14 | } 15 | } 16 | }`; 17 | /** 18 | * @param {string} pageType 19 | * @param {number} entityId 20 | * @returns {string}>} 21 | */ 22 | const renderedRegionsByPageTypeAndEntityIdQuery = (pageType, entityId) => `query { 23 | site { 24 | content { 25 | renderedRegionsByPageTypeAndEntityId(entityPageType: ${pageType}, entityId: ${entityId}) { 26 | regions { 27 | name 28 | html 29 | } 30 | } 31 | } 32 | } 33 | }`; 34 | export { renderedRegionsByPageTypeQuery }; 35 | export { renderedRegionsByPageTypeAndEntityIdQuery }; 36 | export default { 37 | renderedRegionsByPageTypeQuery, 38 | renderedRegionsByPageTypeAndEntityIdQuery, 39 | }; 40 | -------------------------------------------------------------------------------- /lib/lang-assembler.js: -------------------------------------------------------------------------------- 1 | import async from 'async'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const LOCALE_DIRECTORY = 'lang'; 6 | /** 7 | * Assembles together all of the lang files. 8 | * This simply loads the files from the lang directory and puts them in an object where 9 | * locale name is the key and locale data is the value 10 | * 11 | * @param callback 12 | */ 13 | function assemble(callback) { 14 | fs.readdir(LOCALE_DIRECTORY, (err, localeFiles) => { 15 | if (err) { 16 | callback(err); 17 | return; 18 | } 19 | // Ignore hidden files 20 | // @example: MAC generates .DS_STORE 21 | const filteredLocaleFiles = localeFiles.filter((fileName) => fileName[0] !== '.'); 22 | const localesToLoad = {}; 23 | for (const localeFile of filteredLocaleFiles) { 24 | const localeName = path.basename(localeFile, '.json'); 25 | localesToLoad[localeName.toLowerCase()] = (cb2) => { 26 | const localeFilePath = `${LOCALE_DIRECTORY}/${localeFile}`; 27 | fs.readFile(localeFilePath, 'utf-8', cb2); 28 | }; 29 | } 30 | async.parallel(localesToLoad, callback); 31 | }); 32 | } 33 | 34 | export default { 35 | assemble, 36 | LOCALE_DIRECTORY, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/lang-assembler.spec.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import langAssemble from './lang-assembler.js'; 4 | 5 | describe('lang-assembler', () => { 6 | const langFiles = ['en.json', 'pt-BR.json', 'pt.json', 'fr.json', 'fr-CA.json']; 7 | beforeEach(() => { 8 | jest.spyOn(fs, 'readdir').mockImplementation((dir, cb) => { 9 | cb(null, langFiles); 10 | }); 11 | jest.spyOn(fs, 'readFile').mockImplementation((filename, encoding, cb) => { 12 | cb(null, filename); 13 | }); 14 | }); 15 | it('should run lang assemble task successfully', async () => { 16 | langAssemble.assemble((err, result) => { 17 | const keys = Object.keys(result); 18 | expect(keys).toHaveLength(langFiles.length); 19 | }); 20 | }); 21 | it('should return lower case lang keys', () => { 22 | langAssemble.assemble((err, result) => { 23 | Object.keys(result).forEach((lang) => { 24 | expect(lang).toEqual(lang.toLowerCase()); 25 | }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/lang-helper.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import fsUtilsModule from './utils/fsUtils.js'; 3 | 4 | function flattenObject(object, result = {}, parentKey = '') { 5 | return Object.entries(object).reduce((currentLayer, [key, innerValue]) => { 6 | const resultKey = parentKey !== '' ? `${parentKey}.${key}` : key; 7 | if (typeof innerValue === 'object') { 8 | return flattenObject(innerValue, currentLayer, resultKey); 9 | } 10 | // eslint-disable-next-line no-param-reassign 11 | currentLayer[resultKey] = innerValue; 12 | return currentLayer; 13 | }, result); 14 | } 15 | class LangHelper { 16 | constructor({ fsUtils = fsUtilsModule, logger = console } = {}) { 17 | this._fsUtils = fsUtils; 18 | this._logger = logger; 19 | } 20 | 21 | async checkLangKeysPresence(filesPaths, themeLang) { 22 | const themeLangFilename = `${themeLang}.json`; 23 | const defaultLangFilePath = filesPaths.find((filePath) => 24 | filePath.includes(themeLangFilename), 25 | ); 26 | const defaultLangFile = await this._fsUtils.parseJsonFile(defaultLangFilePath); 27 | const flattenedDefaultLang = flattenObject(defaultLangFile); 28 | const alreadyWarnedKey = []; 29 | for await (const filePath of filesPaths) { 30 | if (!filePath.includes(themeLangFilename)) { 31 | const langFile = await this._fsUtils.parseJsonFile(filePath); 32 | const flattenedLang = flattenObject(langFile); 33 | const langKeys = Object.keys(flattenedLang); 34 | for (const langKey of langKeys) { 35 | if (!alreadyWarnedKey.includes(langKey)) { 36 | if (!flattenedDefaultLang[langKey]) { 37 | this._logger.log( 38 | `${ 39 | 'Warning'.yellow 40 | }: file: ${defaultLangFilePath} doesn't have ${langKey}, which is present in ${filePath}`, 41 | ); 42 | } else if (flattenedDefaultLang[langKey].trim().length === 0) { 43 | this._logger.log( 44 | `${ 45 | 'Warning'.yellow 46 | }: file: ${defaultLangFilePath} has ${langKey}, but it's empty`, 47 | ); 48 | } 49 | alreadyWarnedKey.push(langKey); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | export default LangHelper; 57 | -------------------------------------------------------------------------------- /lib/lang/validator.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { recursiveReadDir } from '../utils/fsUtils.js'; 5 | 6 | const LANG_HELPER_REGEXP = /{{\s*lang\s*(?:'|")((?:\w*(?:-\w*)*(\.\w*(?:-\w*)*)*)+)/gim; 7 | class LangpathsValidator { 8 | /** 9 | * 10 | * @param {String} themePath 11 | */ 12 | constructor(themePath) { 13 | this.themePath = themePath; 14 | } 15 | 16 | async run(defaultLang = null) { 17 | const templatesPath = path.join(this.themePath, 'templates'); 18 | const paths = await this.getLangHelpersPaths(templatesPath); 19 | const dedupePaths = [...new Set(paths)]; 20 | const langFiles = await this.getLangFilesContent(defaultLang); 21 | const errors = this.validate(dedupePaths, langFiles); 22 | this.printErrors(errors); 23 | return errors; 24 | } 25 | 26 | printErrors(errors) { 27 | if (errors.length > 0) { 28 | console.log( 29 | 'Warning: Your theme has some missing translations used in the theme:'.yellow, 30 | ); 31 | console.log(errors.join('\n').yellow); 32 | } 33 | } 34 | 35 | searchLangPaths(fileContent, langPath) { 36 | const keys = langPath.split('.'); 37 | let value = fileContent; 38 | for (const key of keys) { 39 | // eslint-disable-next-line no-prototype-builtins 40 | if (value && value.hasOwnProperty(key)) { 41 | value = value[key]; 42 | } else { 43 | return false; 44 | } 45 | } 46 | return value; 47 | } 48 | 49 | validate(paths, langFiles) { 50 | const errors = [ 51 | ...this.checkLangFiles(langFiles), 52 | ...this.checkForMissingTranslations(paths, langFiles), 53 | ]; 54 | return errors; 55 | } 56 | 57 | checkForMissingTranslations(paths, langFiles) { 58 | const errors = []; 59 | for (const langPath of paths) { 60 | // eslint-disable-next-line no-restricted-syntax,guard-for-in 61 | for (const langFile in langFiles) { 62 | const translation = this.searchLangPaths(langFiles[langFile], langPath); 63 | if (!translation) { 64 | errors.push(`Missing translation for ${langPath} in ${langFile}`); 65 | } 66 | } 67 | } 68 | return errors; 69 | } 70 | 71 | checkLangFiles(files) { 72 | if (files.length === 0) { 73 | return ['No lang files found in your theme']; 74 | } 75 | return []; 76 | } 77 | 78 | async getLangHelpersPaths(templatesPath) { 79 | const files = await recursiveReadDir(templatesPath); 80 | const paths = []; 81 | for await (const file of files) { 82 | const content = await fs.promises.readFile(file, { encoding: 'utf-8' }); 83 | const result = content.matchAll(LANG_HELPER_REGEXP); 84 | const arr = [...result]; 85 | if (arr.length > 0) { 86 | const langPath = arr[0][1]; 87 | paths.push(langPath); 88 | } 89 | } 90 | return paths; 91 | } 92 | 93 | async getLangFilesContent(defaultLang = null) { 94 | const filesContent = {}; 95 | const langPath = path.join(this.themePath, 'lang'); 96 | let files = await recursiveReadDir(langPath); 97 | if (defaultLang) { 98 | files = files.filter((file) => file.includes(defaultLang)); 99 | } 100 | for await (const file of files) { 101 | const content = await fs.promises.readFile(file, { encoding: 'utf-8' }); 102 | filesContent[file] = JSON.parse(content); 103 | } 104 | return filesContent; 105 | } 106 | } 107 | export default LangpathsValidator; 108 | -------------------------------------------------------------------------------- /lib/lang/validator.spec.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import LangFilesValidator from './validator.js'; 3 | 4 | describe('lang/validator.js tests', () => { 5 | describe('valid', () => { 6 | it('run with no errors', async () => { 7 | const themePath = path.join(process.cwd(), 'test/_mocks/themes/valid'); 8 | const validator = new LangFilesValidator(themePath); 9 | const errors = await validator.run(); 10 | expect(errors).toHaveLength(0); 11 | }); 12 | it('run with no errors providing default lang', async () => { 13 | const themePath = path.join(process.cwd(), 'test/_mocks/themes/valid'); 14 | const validator = new LangFilesValidator(themePath); 15 | const errors = await validator.run('en'); 16 | expect(errors).toHaveLength(0); 17 | }); 18 | }); 19 | describe('not valid', () => { 20 | it('run with lang helper that is not presented in lang file', async () => { 21 | const themePath = path.join(process.cwd(), 'test/_mocks/themes/invalid-translations'); 22 | const validator = new LangFilesValidator(themePath); 23 | const errors = await validator.run(); 24 | expect(errors).toHaveLength(1); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/nodeSass/BaseFixer.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import postcss from 'postcss'; 4 | import postcssScss from 'postcss-scss'; 5 | 6 | class BaseFixer { 7 | constructor(dirname, brokenFile) { 8 | this.filePath = this.resolveScssFileName(dirname, brokenFile); 9 | } 10 | 11 | async processCss(data, plugin) { 12 | const processor = postcss([plugin]); 13 | return processor.process(data, { from: undefined, parser: postcssScss }); 14 | } 15 | 16 | findImportedFile(file, originalFilePath) { 17 | const originalDirname = path.dirname(originalFilePath); 18 | return this.resolveScssFileName(originalDirname, file); 19 | } 20 | 21 | resolveScssFileName(dirname, fileName) { 22 | if (!fileName.includes('.scss')) { 23 | /* eslint-disable-next-line no-param-reassign */ 24 | fileName += '.scss'; 25 | } 26 | const filePath = path.join(dirname, fileName); 27 | if (!fs.existsSync(filePath)) { 28 | // try with underscore 29 | const fileNameWithUnderscore = this.getFileNameWithUnderscore(fileName); 30 | const filePathWithUnderscore = path.join(dirname, fileNameWithUnderscore); 31 | if (!fs.existsSync(filePathWithUnderscore)) { 32 | throw new Error( 33 | `Import ${fileName} wasn't resolved in ${filePath} or ${filePathWithUnderscore}`, 34 | ); 35 | } 36 | return filePathWithUnderscore; 37 | } 38 | return filePath; 39 | } 40 | 41 | getFileNameWithUnderscore(fileName) { 42 | const fileNameParts = fileName.split('/'); 43 | const fileNameWithUnderscore = fileNameParts 44 | .map((part, i) => { 45 | if (i === fileNameParts.length - 1) { 46 | return '_' + part; 47 | } 48 | return part; 49 | }) 50 | .join('/'); 51 | return fileNameWithUnderscore; 52 | } 53 | } 54 | export default BaseFixer; 55 | -------------------------------------------------------------------------------- /lib/nodeSass/BaseRulesFixer.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import BaseFixer from './BaseFixer.js'; 3 | 4 | class BaseRulesFixer extends BaseFixer { 5 | async run() { 6 | const scss = fs.readFileSync(this.filePath, 'utf8'); 7 | const processedFile = await this.processCss(scss, this.transform()); 8 | return [{ filePath: this.filePath, data: processedFile.css }]; 9 | } 10 | 11 | transform() { 12 | const self = this; 13 | return { 14 | postcssPlugin: 'Transform Base Rules Issues into Comments', 15 | Rule(rule, { Comment }) { 16 | if ( 17 | rule.parent.type === 'root' && 18 | (rule.selector.startsWith('&') || 19 | rule.selector.startsWith(':not(&)') || 20 | rule.selector.startsWith('* &')) 21 | ) { 22 | const comment = new Comment({ text: self.replaceInnerComments(rule) }); 23 | rule.replaceWith(comment); 24 | } 25 | }, 26 | }; 27 | } 28 | 29 | // when we replace rule with comment, there might be a case when comment is inside another rule 30 | // which breaks the commenting root rule 31 | replaceInnerComments(rule) { 32 | return rule.toString().replace(/\/\*(.*)\*\//g, '//$1'); 33 | } 34 | } 35 | export default BaseRulesFixer; 36 | -------------------------------------------------------------------------------- /lib/nodeSass/CommaRemovalFixer.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import BaseFixer from './BaseFixer.js'; 3 | 4 | class CommaRemovalFixer extends BaseFixer { 5 | async run() { 6 | const scss = fs.readFileSync(this.filePath, 'utf8'); 7 | const processedFile = await this.processCss(scss, this.transform()); 8 | return [{ filePath: this.filePath, data: processedFile.css }]; 9 | } 10 | 11 | transform() { 12 | return { 13 | postcssPlugin: 'Unwanted comma removal', 14 | Rule(rule) { 15 | if (rule.selector.startsWith(',') || rule.selector.endsWith(',')) { 16 | /* eslint-disable-next-line no-param-reassign */ 17 | rule.selector = rule.selector.trim().replace(/^,?|,?$/g, ''); 18 | } 19 | }, 20 | }; 21 | } 22 | } 23 | export default CommaRemovalFixer; 24 | -------------------------------------------------------------------------------- /lib/nodeSass/UndefinedVariableFixer.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import BaseFixer from './BaseFixer.js'; 3 | 4 | class UndefinedVariableFixer extends BaseFixer { 5 | async run(errorMessage) { 6 | const varName = this.getUndefinedVariableName(errorMessage); 7 | const value = await this.guessVariableValue(varName); 8 | const scss = fs.readFileSync(this.filePath, 'utf8'); 9 | const processedFile = await this.processCss(scss, this.transform(varName, value)); 10 | return [{ filePath: this.filePath, data: processedFile.css }]; 11 | } 12 | 13 | getUndefinedVariableName(errorMessage) { 14 | const match = errorMessage.match(/\$[a-zA-Z_-]+/gi); 15 | if (!match) { 16 | throw new Error("Couldn't detemine undefined variable name!"); 17 | } 18 | return match[0]; 19 | } 20 | 21 | transform(varName, value) { 22 | return { 23 | postcssPlugin: 'Declare unvariable variable value', 24 | Once(root, { Declaration }) { 25 | const newRule = new Declaration({ 26 | value, 27 | prop: varName, 28 | source: '', 29 | }); 30 | root.prepend(newRule); 31 | }, 32 | }; 33 | } 34 | 35 | async guessVariableValue(varName) { 36 | const scss = fs.readFileSync(this.filePath, 'utf8'); 37 | const processedFile = await this.processCss( 38 | scss, 39 | this.transformForGuessingVariableValue(varName), 40 | ); 41 | return processedFile.varValue; 42 | } 43 | 44 | transformForGuessingVariableValue(varName) { 45 | return { 46 | postcssPlugin: 'Get first variable value found in the file', 47 | Declaration: (decl, { result }) => { 48 | if (decl.prop === varName) { 49 | // eslint-disable-next-line no-param-reassign 50 | result.varValue = decl.value; 51 | // todo propertly break on first found declaration 52 | } 53 | }, 54 | }; 55 | } 56 | } 57 | export default UndefinedVariableFixer; 58 | -------------------------------------------------------------------------------- /lib/parse-json.js: -------------------------------------------------------------------------------- 1 | import parseJson from 'parse-json'; 2 | 3 | export function parse(jsonString, file = '') { 4 | try { 5 | return parseJson(jsonString); 6 | } catch (e) { 7 | throw new Error(`${file} - ${e.message}`); 8 | } 9 | } 10 | export default { 11 | parse, 12 | }; 13 | -------------------------------------------------------------------------------- /lib/parse-json.spec.js: -------------------------------------------------------------------------------- 1 | import { parse } from './parse-json.js'; 2 | 3 | describe('json-lint', () => { 4 | const badJsonFilename = '/path/to/badfile.json'; 5 | const badJson = '{"foo":"bar" "fizz": "buzz"}'; 6 | const file = new RegExp(badJsonFilename); 7 | it('should add file name to error', () => { 8 | const throws = () => { 9 | parse(badJson, badJsonFilename); 10 | }; 11 | expect(throws).toThrow(Error, file); 12 | }); 13 | it('should not need a file name', () => { 14 | const throws = () => { 15 | parse(badJson); 16 | }; 17 | expect(throws).toThrow(Error, !file); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/regions.spec.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import { promisify } from 'util'; 4 | import { parseRegions } from './regions.js'; 5 | import StencilBundle from './stencil-bundle.js'; 6 | 7 | const themePath = `${process.cwd()}/test/_mocks/themes/regions`; 8 | describe('Stencil Bundle', () => { 9 | let bundle; 10 | beforeEach(() => { 11 | const themeConfigStub = { 12 | configExists: jest.fn().mockReturnValue(true), 13 | getRawConfig: jest.fn().mockResolvedValue({}), 14 | getSchema: jest.fn().mockResolvedValue(null), 15 | }; 16 | const rawConfig = { 17 | name: 'Cornerstone', 18 | version: '1.1.0', 19 | }; 20 | jest.spyOn(console, 'log').mockImplementation(jest.fn()); // Prevent littering the console with info messages 21 | jest.spyOn(fs, 'writeFile').mockImplementation((path, data, cb) => cb(null)); 22 | bundle = new StencilBundle(themePath, themeConfigStub, rawConfig, { marketplace: false }); 23 | }); 24 | afterEach(() => { 25 | jest.restoreAllMocks(); 26 | }); 27 | it('should return all regions with the right order.', async () => { 28 | const assembleTemplatesTask = bundle.assembleTemplatesTask.bind(bundle); 29 | const generateManifest = promisify(bundle.generateManifest.bind(bundle)); 30 | const templates = await assembleTemplatesTask(); 31 | const manifest = await generateManifest({ templates }); 32 | expect(manifest.regions['pages/page']).toEqual([ 33 | { name: 'top_region' }, 34 | { name: 'dynamic_a' }, 35 | { name: 'dynamic_b' }, 36 | { name: 'dynamic_c' }, 37 | { name: 'middle_region' }, 38 | { name: 'other' }, 39 | { name: 'bottom_region' }, 40 | ]); 41 | }); 42 | }); 43 | describe('Regions', () => { 44 | describe('parseRegions', () => { 45 | const map = { 46 | '{{{region translation="i18n.RegionName.TestingTranslation" name="_foobar"}}}': [ 47 | { name: '_foobar', translation: 'i18n.RegionName.TestingTranslation' }, 48 | ], 49 | '{{{ region name="foo-bar" translation="i18n.RegionName.Testing-translations" }}}': [ 50 | { name: 'foo-bar', translation: 'i18n.RegionName.Testing-translations' }, 51 | ], 52 | '{{{ region name="foobar__" translation="testing-without-i18n-prefix"}}}': [ 53 | { name: 'foobar__' }, 54 | ], 55 | '{{{ region name="foo_bar" }}}': [{ name: 'foo_bar' }], 56 | '{{{ region name="foo-_bar" }}}': [{ name: 'foo-_bar' }], 57 | '{{{ region name="foobar1" }}}': [{ name: 'foobar1' }], 58 | '{{{ region name=" " }}}': [], 59 | '{{{ region name="invalid name" translation="i18n.RegionName.ValidTranslation"}}}': [], 60 | '{{ region name="two_brackets" }}': [], 61 | "{{{ region name='foobar' }}}": [{ name: 'foobar' }], 62 | '{{{region name="foobar" type="widget"}}}': [{ name: 'foobar' }], 63 | '{{{ region type="widget" name="foobar" }}}': [{ name: 'foobar' }], 64 | '{{{ region name="foobar" type="widget" }}}': [{ name: 'foobar' }], 65 | '{{{ region name=\'foo\' }}} \n {{{ region name="bar" }}}': [ 66 | { name: 'foo' }, 67 | { name: 'bar' }, 68 | ], 69 | '{{{region name=\'foo\'}}}{{{region name="bar"}}}{{{region name="foo"}}}': [ 70 | { name: 'foo' }, 71 | { name: 'bar' }, 72 | ], 73 | }; 74 | for (const template of Object.keys(map)) { 75 | it(`should parse region for template ${template}`, () => { 76 | expect(parseRegions(template)).toEqual(map[template]); 77 | }); 78 | } 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /lib/release/questions.js: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import semver from 'semver'; 3 | 4 | const dateFormatOptions = { 5 | year: 'numeric', 6 | month: '2-digit', 7 | day: '2-digit', 8 | }; 9 | async function askQuestions(currentVersion, githubToken, remotes) { 10 | const remoteChoices = remotes.map((remote) => { 11 | return { value: remote, name: `${remote.name}: ${remote.url}` }; 12 | }); 13 | const nextPatchVersion = semver.inc(currentVersion, 'patch'); 14 | const nextMinorVersion = semver.inc(currentVersion, 'minor'); 15 | const nextMajorVersion = semver.inc(currentVersion, 'major'); 16 | const nextReleaseCandidate = currentVersion.includes('-rc.') 17 | ? semver.inc(currentVersion, 'prerelease', 'rc') 18 | : `${nextMinorVersion}-rc.1`; 19 | const questions = [ 20 | { 21 | name: 'version', 22 | type: 'list', 23 | message: 24 | 'What type of release would you like to do? ' + 25 | 'Current version: '.cyan + 26 | currentVersion, 27 | choices: [ 28 | { 29 | name: 30 | 'Release Candidate: '.yellow + 31 | nextReleaseCandidate.yellow + 32 | ' Internal release for testing.', 33 | value: nextReleaseCandidate, 34 | }, 35 | { 36 | name: 37 | 'Patch: '.yellow + 38 | nextPatchVersion.yellow + 39 | ' Backwards-compatible bug fixes.', 40 | value: nextPatchVersion, 41 | }, 42 | { 43 | name: 44 | 'Minor: '.yellow + 45 | nextMinorVersion.yellow + 46 | ' Feature release or significant update.', 47 | value: nextMinorVersion, 48 | }, 49 | { 50 | name: 'Major: '.yellow + nextMajorVersion.yellow + ' Major change.', 51 | value: nextMajorVersion, 52 | }, 53 | { 54 | name: 'Custom: ?.?.?'.yellow + ' Specify version...', 55 | value: 'custom', 56 | }, 57 | ], 58 | }, 59 | { 60 | name: 'version', 61 | type: 'input', 62 | message: 'What specific version would you like', 63 | askAnswered: true, 64 | when: (answers) => answers.version === 'custom', 65 | validate: (value) => { 66 | const valid = semver.valid(value) && true; 67 | return valid || 'Must be a valid semver, such as 1.2.3'; 68 | }, 69 | }, 70 | { 71 | name: 'remote', 72 | type: 'list', 73 | message: 'What git remote repository would you like to push the release to?', 74 | choices: remoteChoices, 75 | }, 76 | { 77 | name: 'createGithubRelease', 78 | type: 'confirm', 79 | message: 'Create a github release and upload the bundle zip file?', 80 | }, 81 | { 82 | name: 'githubToken', 83 | type: 'input', 84 | message: 'Github token?', 85 | filter: (val) => val.trim(), 86 | when: (answers) => { 87 | return answers.createGithubRelease && !githubToken; 88 | }, 89 | }, 90 | { 91 | name: 'proceed', 92 | type: 'confirm', 93 | message: 'Proceed?', 94 | }, 95 | ]; 96 | const answers = await inquirer.prompt(questions); 97 | if (!answers.proceed) { 98 | throw new Error('Operation cancelled'); 99 | } 100 | answers.githubToken = answers.githubToken || githubToken; 101 | answers.date = new Date().toLocaleString('en-US', dateFormatOptions).split('/').join('-'); 102 | return answers; 103 | } 104 | export default askQuestions; 105 | -------------------------------------------------------------------------------- /lib/schemas/schemaTranslations.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://themes.bigcommerce.com/theme_packages/editorSchemaTranslations", 3 | "title": "Theme translations", 4 | "description": "Translations of strings in schema.json file of a theme", 5 | "type": "object", 6 | "patternProperties": { 7 | "^i18n.": { 8 | "type": "object", 9 | "properties": { 10 | "default": { 11 | "type": "string", 12 | "minLength": 1 13 | } 14 | }, 15 | "patternProperties": { 16 | "[a-z]{2}(-[a-zA-Z0-9]{2,})?$": { 17 | "type": "string", 18 | "minLength": 1 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "required": ["default"] 23 | }, 24 | "^i18n.Bigcommerce.": { 25 | "type": "object", 26 | "properties": { 27 | "default": { 28 | "type": "string", 29 | "minLength": 1 30 | } 31 | }, 32 | "patternProperties": { 33 | "[a-z]{2}(-[a-zA-Z0-9]{2,})?$": { 34 | "type": "string", 35 | "minLength": 1 36 | } 37 | }, 38 | "additionalProperties": false, 39 | "required": ["default"] 40 | } 41 | }, 42 | "additionalProperties": false 43 | } 44 | -------------------------------------------------------------------------------- /lib/spinner.js: -------------------------------------------------------------------------------- 1 | const spinner = async (action, options) => { 2 | // eslint-disable-next-line node/no-unsupported-features/es-syntax 3 | const { oraPromise } = await import('ora'); 4 | return oraPromise(action, { 5 | spinner: 'triangle', 6 | ...options, 7 | }); 8 | }; 9 | export default spinner; 10 | -------------------------------------------------------------------------------- /lib/stencil-download.js: -------------------------------------------------------------------------------- 1 | import async from 'async'; 2 | import stencilPushUtils from './stencil-push.utils.js'; 3 | import stencilPullUtils from './stencil-pull.utils.js'; 4 | import stencilDownloadUtil from './stencil-download.utils.js'; 5 | 6 | function stencilDownload(options) { 7 | return async.waterfall([ 8 | async.constant(options), 9 | stencilPushUtils.readStencilConfigFile, 10 | stencilPushUtils.getStoreHash, 11 | stencilPushUtils.getChannels, 12 | stencilPushUtils.promptUserForChannel, 13 | stencilPullUtils.getChannelActiveTheme, 14 | stencilDownloadUtil.startThemeDownloadJob, 15 | stencilPushUtils.pollForJobCompletion(({ download_url: downloadUrl }) => ({ downloadUrl })), 16 | stencilDownloadUtil.downloadThemeFiles, 17 | ]); 18 | } 19 | export default stencilDownload; 20 | -------------------------------------------------------------------------------- /lib/stencil-download.utils.js: -------------------------------------------------------------------------------- 1 | import * as tmp from 'tmp-promise'; 2 | import { extractZipFiles } from './archiveManager.js'; 3 | import NetworkUtils from './utils/NetworkUtils.js'; 4 | import themeApiClient from './theme-api-client.js'; 5 | 6 | const networkUtils = new NetworkUtils(); 7 | const utils = {}; 8 | utils.downloadThemeFiles = async (options) => { 9 | const { path: tempThemePath, cleanup } = await tmp.file(); 10 | try { 11 | await networkUtils.fetchFile(options.downloadUrl, tempThemePath); 12 | } catch (err) { 13 | throw new Error( 14 | `Unable to download theme files from ${options.downloadUrl}: ${err.message}`, 15 | ); 16 | } 17 | console.log(`${'ok'.green} -- Theme files downloaded`); 18 | console.log(`${'ok'.green} -- Extracting theme files`); 19 | await extractZipFiles({ 20 | zipPath: tempThemePath, 21 | fileToExtract: options.file, 22 | exclude: options.exclude, 23 | }); 24 | console.log(`${'ok'.green} -- Theme files extracted`); 25 | await cleanup(); 26 | return options; 27 | }; 28 | utils.startThemeDownloadJob = async (options) => { 29 | const { 30 | config: { accessToken }, 31 | activeTheme, 32 | storeHash, 33 | } = options; 34 | const apiHost = options.apiHost || options.config.apiHost; 35 | const { jobId } = await themeApiClient.downloadTheme({ 36 | accessToken, 37 | themeId: activeTheme.active_theme_uuid, 38 | apiHost, 39 | storeHash, 40 | }); 41 | return { 42 | ...options, 43 | jobId, 44 | }; 45 | }; 46 | export default utils; 47 | -------------------------------------------------------------------------------- /lib/stencil-pull.js: -------------------------------------------------------------------------------- 1 | import async from 'async'; 2 | import stencilPushUtils from './stencil-push.utils.js'; 3 | import stencilPullUtils from './stencil-pull.utils.js'; 4 | 5 | function stencilPull(options = {}, callback) { 6 | async.waterfall( 7 | [ 8 | async.constant(options), 9 | stencilPushUtils.readStencilConfigFile, 10 | stencilPushUtils.getStoreHash, 11 | stencilPushUtils.getChannels, 12 | stencilPushUtils.promptUserForChannel, 13 | stencilPullUtils.getChannelActiveTheme, 14 | stencilPullUtils.getThemeConfiguration, 15 | stencilPullUtils.getCurrentVariation, 16 | stencilPullUtils.mergeThemeConfiguration, 17 | ], 18 | callback, 19 | ); 20 | } 21 | export default stencilPull; 22 | -------------------------------------------------------------------------------- /lib/stencil-push.js: -------------------------------------------------------------------------------- 1 | import async from 'async'; 2 | import utils from './stencil-push.utils.js'; 3 | 4 | function stencilPush(options = {}, callback) { 5 | async.waterfall( 6 | [ 7 | async.constant(options), 8 | utils.readStencilConfigFile, 9 | utils.getStoreHash, 10 | utils.getThemes, 11 | utils.generateBundle, 12 | utils.uploadBundle, 13 | utils.notifyUserOfThemeLimitReachedIfNecessary, 14 | utils.promptUserToDeleteThemesIfNecessary, 15 | utils.deleteThemesIfNecessary, 16 | utils.checkIfDeletionIsComplete(), 17 | utils.uploadBundleAgainIfNecessary, 18 | utils.notifyUserOfThemeUploadCompletion, 19 | utils.pollForJobCompletion((data) => ({ themeId: data.theme_id })), 20 | utils.promptUserWhetherToApplyTheme, 21 | utils.getChannels, 22 | utils.promptUserForChannels, 23 | utils.getVariations, 24 | utils.promptUserForVariation, 25 | utils.requestToApplyVariationWithRetrys(), 26 | utils.notifyUserOfCompletion, 27 | ], 28 | callback, 29 | ); 30 | } 31 | export default stencilPush; 32 | -------------------------------------------------------------------------------- /lib/store-settings-api-client.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import NetworkUtils from './utils/NetworkUtils.js'; 3 | 4 | const networkUtils = new NetworkUtils(); 5 | async function getStoreSettingsLocale({ apiHost, storeHash, accessToken }) { 6 | try { 7 | const response = await networkUtils.sendApiRequest({ 8 | url: `${apiHost}/stores/${storeHash}/v3/settings/store/locale`, 9 | accessToken, 10 | }); 11 | if (!response.data.data) { 12 | throw new Error('Received empty store locale in the server response'.red); 13 | } else if (!response.data.data.default_shopper_language) { 14 | throw new Error( 15 | 'Received empty default_shopper_language field in the server response'.red, 16 | ); 17 | } else if (!response.data.data.shopper_language_selection_method) { 18 | throw new Error( 19 | 'Received empty shopper_language_selection_method field in the server response'.red, 20 | ); 21 | } 22 | return response.data.data; 23 | } catch (err) { 24 | err.name = 'StoreSettingsLocaleError'; 25 | throw err; 26 | } 27 | } 28 | export { getStoreSettingsLocale }; 29 | export default { 30 | getStoreSettingsLocale, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/utils/NetworkUtils.js: -------------------------------------------------------------------------------- 1 | import https from 'https'; 2 | import fsModule from 'fs'; 3 | import axios from 'axios'; 4 | import { PACKAGE_INFO } from '../../constants.js'; 5 | import stencilCLISettings from '../StencilCLISettings.js'; 6 | 7 | const defaultHttpsAgent = new https.Agent({ rejectUnauthorized: false }); 8 | class NetworkUtils { 9 | constructor({ 10 | fs = fsModule, 11 | httpsAgent = defaultHttpsAgent, 12 | reqLibrary = axios, 13 | packageInfo = PACKAGE_INFO, 14 | logger = console, 15 | } = {}) { 16 | this._fs = fs; 17 | this._httpsAgent = httpsAgent; 18 | this._reqLibrary = reqLibrary; 19 | this._packageInfo = packageInfo; 20 | this._logger = logger; 21 | } 22 | 23 | /** Used to send request to our (Bigcommerce) servers only. 24 | * Shouldn't be used to send requests to third party servers because we disable https checks 25 | * 26 | * @param {object} options 27 | * @param {string} options.url 28 | * @param {object} [options.headers] 29 | * @param {string} [options.accessToken] 30 | * @returns {Promise} 31 | */ 32 | async sendApiRequest(options) { 33 | const { accessToken, ...restOpts } = options; 34 | const reqConfig = { 35 | maxContentLength: Infinity, 36 | maxBodyLength: Infinity, 37 | httpsAgent: this._httpsAgent, 38 | ...restOpts, 39 | headers: { 40 | 'x-auth-client': 'stencil-cli', 41 | 'stencil-cli': this._packageInfo.version, 42 | 'stencil-version': this._packageInfo.config.stencil_version, 43 | ...(restOpts.headers || {}), 44 | }, 45 | }; 46 | if (accessToken) { 47 | reqConfig.headers['x-auth-token'] = accessToken; 48 | } 49 | if (stencilCLISettings.isVerbose()) { 50 | this.log(reqConfig); 51 | } 52 | return this._reqLibrary(reqConfig); 53 | } 54 | 55 | /** 56 | * @param {string} url 57 | * @param {string} outputPath 58 | * @returns {Promise} 59 | */ 60 | async fetchFile(url, outputPath) { 61 | const response = await this.sendApiRequest({ 62 | url, 63 | responseType: 'stream', 64 | }); 65 | return new Promise((resolve, reject) => { 66 | response.data 67 | .pipe(this._fs.createWriteStream(outputPath)) 68 | .on('finish', resolve) 69 | .on('error', reject); 70 | }); 71 | } 72 | 73 | log(config) { 74 | const method = config.method || 'GET'; 75 | this._logger.info(`Upcoming request ${method.green}: ${config.url.green}`); 76 | } 77 | } 78 | export default NetworkUtils; 79 | -------------------------------------------------------------------------------- /lib/utils/asyncUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Contains helpers functions for working with async stuff and streams 3 | */ 4 | /** 5 | * WARNING! Can be used with text content only. Binary data (e.g. images) will get broken! 6 | * 7 | * @param {ReadableStream} stream 8 | * @returns {Promise} 9 | */ 10 | async function readFromStream(stream) { 11 | let result = ''; 12 | for await (const chunk of stream) { 13 | result += chunk; 14 | } 15 | return result; 16 | } 17 | export { readFromStream }; 18 | export default { 19 | readFromStream, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/frontmatter.js: -------------------------------------------------------------------------------- 1 | const frontmatterRegex = /---[\s\S]+?---/g; 2 | /** 3 | * 4 | * @param {String} file 5 | * @returns {String|null} 6 | */ 7 | function getFrontmatterContent(file) { 8 | const frontmatterMatch = file.match(frontmatterRegex); 9 | return frontmatterMatch !== null ? frontmatterMatch[0] : null; 10 | } 11 | /** 12 | * 13 | * @param {String} frontmatter 14 | * @param {Object} settings 15 | * @returns {String} 16 | */ 17 | function interpolateThemeSettings(frontmatter, settings) { 18 | for (const [key, val] of Object.entries(settings)) { 19 | const regex = `{{\\s*?theme_settings\\.${key}\\s*?}}`; 20 | // eslint-disable-next-line no-param-reassign 21 | frontmatter = frontmatter.replace(new RegExp(regex, 'g'), val); 22 | } 23 | return frontmatter; 24 | } 25 | export { frontmatterRegex }; 26 | export { getFrontmatterContent }; 27 | export { interpolateThemeSettings }; 28 | export default { 29 | frontmatterRegex, 30 | getFrontmatterContent, 31 | interpolateThemeSettings, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/utils/frontmatter.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { getFrontmatterContent } from './frontmatter.js'; 3 | 4 | const cwd = process.cwd(); 5 | describe('frontmatter', () => { 6 | it('should successfully get frontmatter content fromt file', async () => { 7 | const fileName = `${cwd}/test/_mocks/frontmatter/valid.html`; 8 | const fileContent = await fs.promises.readFile(fileName, { encoding: 'utf-8' }); 9 | const frontmatter = getFrontmatterContent(fileContent); 10 | expect(frontmatter).not.toBeNull(); 11 | }); 12 | it('should return null while getting frontmatter content fromt file', async () => { 13 | const fileName = `${cwd}/test/_mocks/frontmatter/absent.html`; 14 | const fileContent = await fs.promises.readFile(fileName, { encoding: 'utf-8' }); 15 | const frontmatter = getFrontmatterContent(fileContent); 16 | expect(frontmatter).toBeNull(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lib/utils/fsUtils.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import recursiveReadDir from 'recursive-readdir'; 3 | import { parse } from '../parse-json.js'; 4 | /** 5 | * @param {string} filePath 6 | * @returns {Promise} - parsed JSON content of the file 7 | */ 8 | async function parseJsonFile(filePath) { 9 | const contentStr = await fs.promises.readFile(filePath, { encoding: 'utf-8' }); 10 | // We use parse-json instead of JSON.parse because it throws errors with better explanations what is wrong 11 | return parse(contentStr, filePath); 12 | } 13 | export { parseJsonFile }; 14 | export { recursiveReadDir }; 15 | export default { 16 | ...fs, 17 | parseJsonFile, 18 | recursiveReadDir, 19 | }; 20 | -------------------------------------------------------------------------------- /lib/validator/schema-translations.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | 3 | const fileUrl = new URL('../schemas/schemaTranslations.json', import.meta.url); 4 | const schemaTranslations = JSON.parse(readFileSync(fileUrl)); 5 | 6 | class ValidatorSchemaTranslations { 7 | constructor() { 8 | this.trackedKeys = ['name', 'content', 'label', 'settings', 'options', 'group']; 9 | this.validationSchema = schemaTranslations; 10 | this.translations = {}; 11 | this.schemaKeys = []; 12 | this.translationsKeys = []; 13 | } 14 | 15 | /** 16 | * Set schema.json 17 | * @param {array} schema 18 | */ 19 | setSchema(schema) { 20 | this.getTranslatableStrings(schema); 21 | } 22 | 23 | /** 24 | * Set schemaTranslations.json 25 | * @param {object} translations 26 | */ 27 | setTranslations(translations) { 28 | this.translations = translations; 29 | this.getTranslationsKeys(); 30 | } 31 | 32 | /** 33 | * Set i18n key from a schema into keys array 34 | * @param {string} value 35 | */ 36 | setSchemaKeys(value) { 37 | if (value && !this.schemaKeys.includes(value) && /^i18n\./.test(value)) { 38 | this.schemaKeys.push(value); 39 | } 40 | } 41 | 42 | /** 43 | * Get validation schema 44 | * @returns {object} 45 | */ 46 | getValidationSchema() { 47 | return this.validationSchema; 48 | } 49 | 50 | /** 51 | * Get translations 52 | * @returns {array} 53 | */ 54 | getTranslations() { 55 | return this.translations; 56 | } 57 | 58 | /** 59 | * Get i18n keys from translations 60 | * @returns {array} 61 | */ 62 | getTranslationsKeys() { 63 | this.translationsKeys = Object.keys(this.translations); 64 | } 65 | 66 | /** 67 | * Get i18n keys from schema 68 | * @returns {array} 69 | */ 70 | getSchemaKeys() { 71 | return this.schemaKeys; 72 | } 73 | 74 | /** 75 | * Get translatable strings 76 | * @param {object[]} schema 77 | */ 78 | getTranslatableStrings(schema) { 79 | for (const element of schema) { 80 | for (const [key, value] of Object.entries(element)) { 81 | if (!this.trackedKeys.includes(key)) { 82 | continue; 83 | } 84 | if (Array.isArray(value)) { 85 | this.getTranslatableStrings(value); 86 | } 87 | this.setSchemaKeys(value); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Find unused i18n keys in schemaTranslations 94 | * @returns {array} 95 | */ 96 | findUnusedKeys() { 97 | return this.translationsKeys.filter((key) => { 98 | const translationRegex = /^i18n.RegionName.*?/g; 99 | const translationMatch = translationRegex.exec(key); 100 | if (translationMatch) { 101 | return false; 102 | } 103 | return !this.schemaKeys.includes(key); 104 | }); 105 | } 106 | 107 | /** 108 | * Find missed i18n keys in schemaTranslations 109 | * @returns {array} 110 | */ 111 | findMissedKeys() { 112 | return this.schemaKeys.filter((key) => !this.translationsKeys.includes(key)); 113 | } 114 | } 115 | export default ValidatorSchemaTranslations; 116 | -------------------------------------------------------------------------------- /lib/validator/schema-translations.spec.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import ValidatorSchemaTranslations from './schema-translations.js'; 3 | 4 | const schema = JSON.parse(readFileSync(`${process.cwd()}/test/_mocks/themes/valid/schema.json`)); 5 | const schemaTranslations = JSON.parse( 6 | readFileSync(`${process.cwd()}/test/_mocks/themes/valid/schemaTranslations.json`), 7 | ); 8 | const validationsTranslations = JSON.parse( 9 | readFileSync(`${process.cwd()}/lib/schemas/schemaTranslations.json`), 10 | ); 11 | 12 | const validatorSchemaTranslations = () => { 13 | return new ValidatorSchemaTranslations(); 14 | }; 15 | describe('ValidatorSchemaTranslations', () => { 16 | it('should return translations', () => { 17 | const instance = validatorSchemaTranslations(); 18 | instance.setTranslations(schemaTranslations); 19 | expect(instance.getTranslations()).toEqual(schemaTranslations); 20 | }); 21 | it('should return translations keys', () => { 22 | const instance = validatorSchemaTranslations(); 23 | instance.setSchema(schema); 24 | expect(instance.getSchemaKeys()).toEqual(['i18n.Test']); 25 | }); 26 | it('should return validation schema', () => { 27 | const instance = validatorSchemaTranslations(); 28 | expect(instance.getValidationSchema()).toEqual(validationsTranslations); 29 | }); 30 | it('should return i18n keys array without duplicates', () => { 31 | const instance = validatorSchemaTranslations(); 32 | instance.setSchemaKeys('i18n.Global'); 33 | instance.setSchemaKeys('i18n.Global'); 34 | expect(instance.getSchemaKeys()).toEqual(['i18n.Global']); 35 | }); 36 | it('should return empty i18n keys array if specify empty string', () => { 37 | const instance = validatorSchemaTranslations(); 38 | instance.setSchemaKeys(''); 39 | expect(instance.getSchemaKeys()).toEqual([]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bigcommerce/stencil-cli", 3 | "version": "8.3.0", 4 | "type": "module", 5 | "description": "CLI tool to run BigCommerce Stores locally for theme development.", 6 | "main": "index.js", 7 | "engines": { 8 | "node": ">=18", 9 | "npm": ">=8.0.0" 10 | }, 11 | "scripts": { 12 | "lint": "eslint . && prettier --check . --ignore-unknown", 13 | "lint-and-fix": "eslint . --fix && prettier --write . --ignore-unknown", 14 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 15 | "test-with-coverage": "npm run test --coverage --verbose", 16 | "release": "semantic-release" 17 | }, 18 | "bin": { 19 | "stencil": "./bin/stencil.js", 20 | "stencil-bundle": "./bin/stencil-bundle.js", 21 | "stencil-download": "./bin/stencil-download.js", 22 | "stencil-init": "./bin/stencil-init.js", 23 | "stencil-push": "./bin/stencil-push.js", 24 | "stencil-pull": "./bin/stencil-pull.js", 25 | "stencil-start": "./bin/stencil-start.js", 26 | "stencil-release": "./bin/stencil-release.js", 27 | "stencil-debug": "./bin/stencil-debug.js", 28 | "stencil-scss-autofix": "./bin/stencil-scss-autofix.js" 29 | }, 30 | "config": { 31 | "stencil_version": "1.0" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/bigcommerce/stencil-cli" 36 | }, 37 | "author": "BigCommerce", 38 | "license": "BSD-4-Clause", 39 | "bugs": { 40 | "url": "https://github.com/bigcommerce/stencil-cli/issues" 41 | }, 42 | "homepage": "https://github.com/bigcommerce/stencil-cli", 43 | "files": [ 44 | "/lib", 45 | "/bin", 46 | "/server", 47 | "!**/*.spec.js", 48 | "constants.js", 49 | "package-lock.json" 50 | ], 51 | "dependencies": { 52 | "@bigcommerce/stencil-paper": "5.1.6", 53 | "@bigcommerce/stencil-styles": "^6.1.1", 54 | "@hapi/boom": "^10.0.0", 55 | "@hapi/glue": "^9.0.1", 56 | "@hapi/h2o2": "^9.1.0", 57 | "@hapi/hapi": "^20.2.2", 58 | "@hapi/inert": "^6.0.5", 59 | "@jest/globals": "^29.7.0", 60 | "@octokit/rest": "^21.0.2", 61 | "ajv": "^6.12.5", 62 | "archiver": "^5.0.2", 63 | "async": "^3.2.3", 64 | "axios": "^0.28.0", 65 | "browser-sync": "^2.27.9", 66 | "cheerio": "^1.0.0", 67 | "colors": "1.4.0", 68 | "commander": "^6.1.0", 69 | "confidence": "^5.0.1", 70 | "form-data": "^3.0.0", 71 | "front-matter": "^4.0.2", 72 | "glob": "^7.1.6", 73 | "graceful-fs": "^4.2.4", 74 | "husky": "^8.0.1", 75 | "image-size": "^0.9.1", 76 | "inquirer": "^8.1.5", 77 | "js-yaml": "^4.1.0", 78 | "lodash": "^4.17.20", 79 | "lodash-es": "^4.17.21", 80 | "memory-cache": "^0.2.0", 81 | "npm-which": "^3.0.1", 82 | "nypm": "^0.3.8", 83 | "object-to-spawn-args": "^2.0.0", 84 | "ora": "^8.0.1", 85 | "parse-json": "^5.2.0", 86 | "postcss": "^8.4.21", 87 | "postcss-safe-parser": "^6.0.0", 88 | "postcss-scss": "^4.0.6", 89 | "progress": "^2.0.3", 90 | "recursive-readdir": "^2.2.2", 91 | "semver": "^7.3.2", 92 | "simple-git": "^3.26.0", 93 | "tarjan-graph": "^2.0.0", 94 | "tmp-promise": "^3.0.2", 95 | "upath": "^1.2.0", 96 | "uuid4": "^2.0.2", 97 | "yauzl": "^2.10.0" 98 | }, 99 | "devDependencies": { 100 | "@commitlint/cli": "^17.1.2", 101 | "@commitlint/config-conventional": "^17.1.0", 102 | "@semantic-release/changelog": "^6.0.1", 103 | "@semantic-release/commit-analyzer": "^9.0.2", 104 | "@semantic-release/git": "^10.0.1", 105 | "@semantic-release/github": "^11.0.0", 106 | "@semantic-release/npm": "^9.0.1", 107 | "@semantic-release/release-notes-generator": "^10.0.3", 108 | "axios-mock-adapter": "^1.21.2", 109 | "conventional-changelog-cli": "^2.2.2", 110 | "eslint": "^7.10.0", 111 | "eslint-config-airbnb-base": "^14.2.0", 112 | "eslint-config-prettier": "^6.11.0", 113 | "eslint-plugin-import": "^2.26.0", 114 | "eslint-plugin-jest": "^24.0.2", 115 | "eslint-plugin-node": "^11.1.0", 116 | "eslint-plugin-prettier": "^3.1.4", 117 | "jest": "^29.7.0", 118 | "prettier": "2.1.2", 119 | "semantic-release": "^24.1.1", 120 | "semantic-release-github-pullrequest": "^1.3.0" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | import Confidence from 'confidence'; 2 | 3 | const config = { 4 | $meta: 'Config file', 5 | server: { 6 | host: 'localhost', 7 | port: 3000, 8 | }, 9 | }; 10 | const criteria = { 11 | env: process.env.NODE_ENV || 'development', 12 | }; 13 | const store = new Confidence.Store(config); 14 | export const get = (key) => store.get(key, criteria); 15 | export const meta = (key) => store.meta(key, criteria); 16 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import Glue from '@hapi/glue'; 4 | import * as _ from 'lodash-es'; 5 | import * as manifest from './manifest.js'; 6 | import logo from './lib/show-logo.js'; 7 | import 'colors'; 8 | 9 | const getDirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | function buildManifest(srcManifest, options) { 12 | const resManifest = _.cloneDeep(srcManifest); 13 | const pluginsByName = resManifest.register.plugins; 14 | const parsedSecureUrl = new URL(options.dotStencilFile.storeUrl); // The url to a secure page (prompted as login page) 15 | const parsedNormalUrl = new URL(options.dotStencilFile.normalStoreUrl); // The host url of the homepage; 16 | const storeUrl = parsedSecureUrl.protocol + '//' + parsedSecureUrl.host; 17 | resManifest.server.port = options.dotStencilFile.port; 18 | pluginsByName['./plugins/router/router.module.js'].storeUrl = storeUrl; 19 | pluginsByName['./plugins/router/router.module.js'].normalStoreUrl = 20 | parsedNormalUrl.protocol + '//' + parsedNormalUrl.host; 21 | pluginsByName['./plugins/router/router.module.js'].apiKey = options.dotStencilFile.apiKey; 22 | pluginsByName['./plugins/router/router.module.js'].port = options.dotStencilFile.port; 23 | pluginsByName['./plugins/router/router.module.js'].stencilCliVersion = 24 | options.stencilCliVersion; 25 | pluginsByName['./plugins/router/router.module.js'].accessToken = 26 | options.dotStencilFile.accessToken; 27 | pluginsByName['./plugins/renderer/renderer.module.js'].useCache = options.useCache; 28 | pluginsByName['./plugins/renderer/renderer.module.js'].username = 29 | options.dotStencilFile.username; 30 | pluginsByName['./plugins/renderer/renderer.module.js'].token = options.dotStencilFile.token; 31 | pluginsByName['./plugins/renderer/renderer.module.js'].accessToken = 32 | options.dotStencilFile.accessToken; 33 | pluginsByName['./plugins/renderer/renderer.module.js'].customLayouts = 34 | options.dotStencilFile.customLayouts; 35 | pluginsByName['./plugins/renderer/renderer.module.js'].themePath = options.themePath; 36 | pluginsByName['./plugins/renderer/renderer.module.js'].storeUrl = storeUrl; 37 | pluginsByName['./plugins/renderer/renderer.module.js'].storeSettingsLocale = 38 | options.storeSettingsLocale; 39 | pluginsByName['./plugins/theme-assets/theme-assets.module.js'].themePath = options.themePath; 40 | resManifest.register.plugins = _.reduce( 41 | pluginsByName, 42 | (pluginsArr, opts, plugin) => [...pluginsArr, { plugin, options: opts }], 43 | [], 44 | ); 45 | return resManifest; 46 | } 47 | async function create(options) { 48 | const serverManifest = buildManifest(manifest.get('/'), options); 49 | const server = await Glue.compose(serverManifest, { relativeTo: getDirname }); 50 | await server.start(); 51 | 52 | console.log(logo); 53 | 54 | return server; 55 | } 56 | export default { 57 | create, 58 | }; 59 | -------------------------------------------------------------------------------- /server/lib/page-type-util.js: -------------------------------------------------------------------------------- 1 | const PageTypes = { 2 | PAGE: 'PAGE', 3 | PRODUCT: 'PRODUCT', 4 | CATEGORY: 'CATEGORY', 5 | BRAND: 'BRAND', 6 | ACCOUNT_RETURN_SAVED: 'ACCOUNT_RETURN_SAVED', 7 | ACCOUNT_ADD_RETURN: 'ACCOUNT_ADD_RETURN', 8 | ACCOUNT_RETURNS: 'ACCOUNT_RETURNS', 9 | ACCOUNT_ADD_ADDRESS: 'ACCOUNT_ADD_ADDRESS', 10 | ACCOUNT_ADD_WISHLIST: 'ACCOUNT_ADD_WISHLIST', 11 | ACCOUNT_WISHLISTS: 'ACCOUNT_WISHLISTS', 12 | ACCOUNT_WISHLIST_DETAILS: 'ACCOUNT_WISHLIST_DETAILS', 13 | ACCOUNT_EDIT: 'ACCOUNT_EDIT', 14 | ACCOUNT_ADDRESS: 'ACCOUNT_ADDRESS', 15 | ACCOUNT_INBOX: 'ACCOUNT_INBOX', 16 | ACCOUNT_DOWNLOAD_ITEM: 'ACCOUNT_DOWNLOAD_ITEM', 17 | ACCOUNT_ORDERS_ALL: 'ACCOUNT_ORDERS_ALL', 18 | ACCOUNT_ORDERS_INVOICE: 'ACCOUNT_ORDERS_INVOICE', 19 | ACCOUNT_ORDERS_DETAILS: 'ACCOUNT_ORDERS_DETAILS', 20 | ACCOUNT_ORDERS_COMPLETED: 'ACCOUNT_ORDERS_COMPLETED', 21 | ACCOUNT_RECENT_ITEMS: 'ACCOUNT_RECENT_ITEMS', 22 | AUTH_ACCOUNT_CREATED: 'AUTH_ACCOUNT_CREATED', 23 | AUTH_LOGIN: 'AUTH_LOGIN', 24 | AUTH_CREATE_ACC: 'AUTH_CREATE_ACC', 25 | AUTH_FORGOT_PASS: 'AUTH_FORGOT_PASS', 26 | AUTH_NEW_PASS: 'AUTH_NEW_PASS', 27 | BLOG_POST: 'BLOG_POST', 28 | BLOG: 'BLOG', 29 | BRANDS: 'BRANDS', 30 | CART: 'CART', 31 | COMPARE: 'COMPARE', 32 | CONTACT_US: 'CONTACT_US', 33 | HOME: 'HOME', 34 | GIFT_CERT_PURCHASE: 'GIFT_CERT_PURCHASE', 35 | GIFT_CERT_REDEEM: 'GIFT_CERT_REDEEM', 36 | GIFT_CERT_BALANCE: 'GIFT_CERT_BALANCE', 37 | ORDER_INFO: 'ORDER_INFO', 38 | SEARCH: 'SEARCH', 39 | SITEMAP: 'SITEMAP', 40 | SUBSCRIBED: 'SUBSCRIBED', 41 | UNSUBSCRIBE: 'UNSUBSCRIBE', 42 | }; 43 | const templateFileToPageTypeMap = { 44 | 'pages/page': PageTypes.PAGE, 45 | 'pages/product': PageTypes.PRODUCT, 46 | 'pages/category': PageTypes.CATEGORY, 47 | 'pages/brand': PageTypes.BRAND, 48 | 'pages/account/return-saved': PageTypes.ACCOUNT_RETURN_SAVED, 49 | 'pages/account/add-return': PageTypes.ACCOUNT_ADD_RETURN, 50 | 'pages/account/returns': PageTypes.ACCOUNT_RETURNS, 51 | 'pages/account/add-address': PageTypes.ACCOUNT_ADD_ADDRESS, 52 | 'pages/account/add-wishlist': PageTypes.ACCOUNT_ADD_WISHLIST, 53 | 'pages/account/wishlists': PageTypes.ACCOUNT_WISHLISTS, 54 | 'pages/account/wishlist-details': PageTypes.ACCOUNT_WISHLIST_DETAILS, 55 | 'pages/account/edit': PageTypes.ACCOUNT_EDIT, 56 | 'pages/account/addresses': PageTypes.ACCOUNT_ADDRESS, 57 | 'pages/account/inbox': PageTypes.ACCOUNT_INBOX, 58 | 'pages/account/download-item': PageTypes.ACCOUNT_DOWNLOAD_ITEM, 59 | 'pages/account/orders/all': PageTypes.ACCOUNT_ORDERS_ALL, 60 | 'pages/account/orders/invoice': PageTypes.ACCOUNT_ORDERS_INVOICE, 61 | 'pages/account/orders/details': PageTypes.ACCOUNT_ORDERS_DETAILS, 62 | 'pages/account/orders/completed': PageTypes.ACCOUNT_ORDERS_COMPLETED, 63 | 'pages/account/recent-items': PageTypes.ACCOUNT_RECENT_ITEMS, 64 | 'pages/auth/account-created': PageTypes.AUTH_ACCOUNT_CREATED, 65 | 'pages/auth/login': PageTypes.AUTH_LOGIN, 66 | 'pages/auth/create-account': PageTypes.AUTH_CREATE_ACC, 67 | 'pages/auth/forgot-password': PageTypes.AUTH_FORGOT_PASS, 68 | 'pages/auth/new-password': PageTypes.AUTH_NEW_PASS, 69 | 'pages/blog-post': PageTypes.BLOG_POST, 70 | 'pages/blog': PageTypes.BLOG, 71 | 'pages/brands': PageTypes.BRANDS, 72 | 'pages/cart': PageTypes.CART, 73 | 'pages/compare': PageTypes.COMPARE, 74 | 'pages/contact-us': PageTypes.CONTACT_US, 75 | 'pages/home': PageTypes.HOME, 76 | 'pages/gift-certificate/purchase': PageTypes.GIFT_CERT_PURCHASE, 77 | 'pages/gift-certificate/redeem': PageTypes.GIFT_CERT_REDEEM, 78 | 'pages/gift-certificate/balance': PageTypes.GIFT_CERT_BALANCE, 79 | 'pages/order-confirmation': PageTypes.ORDER_INFO, 80 | 'pages/search': PageTypes.SEARCH, 81 | 'pages/sitemap': PageTypes.SITEMAP, 82 | 'pages/subscribed': PageTypes.SUBSCRIBED, 83 | 'pages/unsubscribe': PageTypes.UNSUBSCRIBE, 84 | }; 85 | /** 86 | * Convert a templateFile to pageType 87 | * 88 | * @param {string} templateFile 89 | * @returns {string | null} 90 | */ 91 | function getPageType(templateFile) { 92 | const pageType = templateFileToPageTypeMap[templateFile]; 93 | return pageType; 94 | } 95 | export { getPageType }; 96 | export default { 97 | getPageType, 98 | }; 99 | -------------------------------------------------------------------------------- /server/lib/page-type-util.spec.js: -------------------------------------------------------------------------------- 1 | import { getPageType } from './page-type-util.js'; 2 | 3 | describe('page-type-util', () => { 4 | describe('getPageType', () => { 5 | it('should return a string pageType value', () => { 6 | expect(getPageType('pages/page')).toEqual('PAGE'); 7 | expect(getPageType('pages/brand')).toEqual('BRAND'); 8 | }); 9 | it('should should return a null value', () => { 10 | expect(getPageType('pages/something')).toBeUndefined(); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /server/lib/show-logo.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | 3 | // eslint-disable-next-line import/no-mutable-exports 4 | let logo = '\n'; 5 | logo += ' `+h\n'.blue; 6 | logo += ' `+ddd\n'.blue; 7 | logo += ' .oddddd\n'.blue; 8 | logo += ' .oddddddd\n'.blue; 9 | logo += ' -sddddddddd\n'.blue; 10 | logo += ' `-sddddddddddd\n'.blue; 11 | logo += ' -shdddddddddddd\n'.blue; 12 | logo += ' ...-:+ydddddddd\n'.blue; 13 | logo += ' `......` `+ddddddd\n'.blue; 14 | logo += ' -ddddddh- ddddddd\n'.blue; 15 | logo += ' ` .yyyyyyo. `+ddddddd\n'.blue; 16 | logo += ' .o/ ````` :ydddddddd\n'.blue; 17 | logo += ' -ohd+ `//////:` `.sddddddd\n'.blue; 18 | logo += ' -sdddd+ -ddddddds `hdddddd\n'.blue; 19 | logo += ' :sdddddd+ .sssssso- `ddddddd\n'.blue; 20 | logo += ' :ydddddddd+ -yddddddd\n'.blue; 21 | logo += ' /yddddddddddy+++++++++++oshddddddddd\n'.blue; 22 | logo += ' `/hdddddddddddddddddddddddddddddddddddd\n'.blue; 23 | logo += '/hdddddddddddddddddddddddddddddddddddddd\n'.blue; 24 | logo += '_________________________\n'.white; 25 | logo += '\n'; 26 | logo += 'BigCommerce Stencil \n'.yellow; 27 | logo += '_________________________\n'.white; 28 | export default logo; 29 | -------------------------------------------------------------------------------- /server/lib/utils.js: -------------------------------------------------------------------------------- 1 | const uuidRegExp = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-([0-9a-f]{12})'; 2 | /** 3 | * Strip domain from the cookies header string 4 | * 5 | * @param {string[]} cookies 6 | * @returns {string[]} 7 | */ 8 | function stripDomainFromCookies(cookies) { 9 | return cookies.map((val) => 10 | val 11 | .replace(/(?:;\s)?domain=(?:.+?)(;|$)/gi, '$1') 12 | .replace(new RegExp('; SameSite=none', 'gi'), ''), 13 | ); 14 | } 15 | /** 16 | * Strip domain from redirectUrl if it matches the current storeUrl, if not, leave it. 17 | * 18 | * @param {string} redirectUrl 19 | * @param {{ normalStoreUrl, storeUrl}} config 20 | * @returns {string} 21 | */ 22 | function normalizeRedirectUrl(redirectUrl, config) { 23 | if (!redirectUrl || !redirectUrl.startsWith('http')) { 24 | return redirectUrl; // already stripped, skip 25 | } 26 | const storeHost = new URL(config.normalStoreUrl).host; 27 | const secureStoreHost = new URL(config.storeUrl).host; 28 | const redirectUrlObj = new URL(redirectUrl); 29 | if (redirectUrlObj.host === storeHost || redirectUrlObj.host === secureStoreHost) { 30 | // Need to strip 31 | return redirectUrlObj.pathname + redirectUrlObj.search + redirectUrlObj.hash; 32 | } 33 | return redirectUrl; // Different host, shouldn't strip 34 | } 35 | /** 36 | * Convert a number to uuid 37 | * 38 | * @param {number} number 39 | * @returns {string} 40 | */ 41 | function int2uuid(number) { 42 | const id = `000000000000${number}`.substr(-12); 43 | return `00000000-0000-0000-0000-${id}`; 44 | } 45 | /** 46 | * Convert a uuid to int 47 | * 48 | * @param {string} uuid 49 | * @returns {number} 50 | */ 51 | function uuid2int(uuid) { 52 | const match = uuid.match(new RegExp(uuidRegExp)); 53 | if (!match) { 54 | throw new Error(`Not uuid match for ${uuid}`); 55 | } 56 | return match ? parseInt(match[1], 10) : 0; 57 | } 58 | export { stripDomainFromCookies }; 59 | export { normalizeRedirectUrl }; 60 | export { int2uuid }; 61 | export { uuid2int }; 62 | export { uuidRegExp }; 63 | export default { 64 | stripDomainFromCookies, 65 | normalizeRedirectUrl, 66 | int2uuid, 67 | uuid2int, 68 | uuidRegExp, 69 | }; 70 | -------------------------------------------------------------------------------- /server/lib/utils.spec.js: -------------------------------------------------------------------------------- 1 | import { uuid2int, int2uuid, normalizeRedirectUrl } from './utils.js'; 2 | 3 | describe('utils', () => { 4 | describe('uuid2int', () => { 5 | it('should return an int value presentation', () => { 6 | expect(uuid2int('00000000-0000-0000-0000-000000000001')).toEqual(1); 7 | expect(uuid2int('00000000-0000-0000-0000-000000000102')).toEqual(102); 8 | }); 9 | it('should throw an error if an invalid uuid is used', () => { 10 | expect(() => uuid2int('00002')).toThrow(Error); 11 | }); 12 | }); 13 | describe('int2uuid', () => { 14 | it('should return an uuid value presentation', () => { 15 | expect(int2uuid(1)).toEqual('00000000-0000-0000-0000-000000000001'); 16 | expect(int2uuid(505)).toEqual('00000000-0000-0000-0000-000000000505'); 17 | }); 18 | }); 19 | describe('normalizeRedirectUrl', () => { 20 | it('should return the original value if the redirectUrl is already striped', () => { 21 | const redirectUrl = '/products?filter=name#3'; 22 | expect(normalizeRedirectUrl(redirectUrl, {})).toEqual(redirectUrl); 23 | }); 24 | it('should return the original value if the redirectUrl has a host different from hosts in config', () => { 25 | const redirectUrl = 'https://google.com/search?q=this-product'; 26 | const config = { 27 | normalStoreUrl: 'https://store-12345678.mybigcommerce.com', 28 | storeUrl: 'https://my-awesome-store.com', 29 | }; 30 | expect(normalizeRedirectUrl(redirectUrl, config)).toEqual(redirectUrl); 31 | }); 32 | it('should return the url without host if the redirectUrl has a host = to normalStoreUrl', () => { 33 | const redirectUrl = 'https://store-12345678.mybigcommerce.com/products?filter=name#3'; 34 | const config = { 35 | normalStoreUrl: 'https://store-12345678.mybigcommerce.com', 36 | storeUrl: 'https://my-awesome-store.com', 37 | }; 38 | expect(normalizeRedirectUrl(redirectUrl, config)).toEqual('/products?filter=name#3'); 39 | }); 40 | it('should return the url without host if the redirectUrl has a host = to storeUrl', () => { 41 | const redirectUrl = 'https://my-awesome-store.com/products?filter=name#3'; 42 | const config = { 43 | normalStoreUrl: 'https://store-12345678.mybigcommerce.com', 44 | storeUrl: 'https://my-awesome-store.com', 45 | }; 46 | expect(normalizeRedirectUrl(redirectUrl, config)).toEqual('/products?filter=name#3'); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /server/manifest.js: -------------------------------------------------------------------------------- 1 | import Confidence from 'confidence'; 2 | import * as config from './config.js'; 3 | 4 | const criteria = { 5 | env: process.env.NODE_ENV, 6 | }; 7 | const manifest = { 8 | $meta: 'Stencil', 9 | server: { 10 | host: config.get('/server/host'), 11 | port: config.get('/server/port'), 12 | }, 13 | register: { 14 | plugins: { 15 | // Third Party Plugins 16 | '@hapi/inert': {}, 17 | '@hapi/h2o2': {}, 18 | // First Party Plugins 19 | './plugins/renderer/renderer.module.js': {}, 20 | './plugins/router/router.module.js': {}, 21 | './plugins/theme-assets/theme-assets.module.js': {}, 22 | }, 23 | }, 24 | }; 25 | const store = new Confidence.Store(manifest); 26 | export const get = (key) => store.get(key, criteria); 27 | export const meta = (key) => store.meta(key, criteria); 28 | -------------------------------------------------------------------------------- /server/plugins/renderer/responses/index.js: -------------------------------------------------------------------------------- 1 | import PencilResponse from './pencil-response.js'; 2 | import RawResponse from './raw-response.js'; 3 | import RedirectResponse from './redirect-response.js'; 4 | 5 | export { PencilResponse }; 6 | export { RawResponse }; 7 | export { RedirectResponse }; 8 | export default { 9 | PencilResponse, 10 | RawResponse, 11 | RedirectResponse, 12 | }; 13 | -------------------------------------------------------------------------------- /server/plugins/renderer/responses/raw-response.js: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import { int2uuid } from '../../../lib/utils.js'; 3 | 4 | const internals = { 5 | stubActiveVersion: int2uuid(1), 6 | stubActiveConfig: int2uuid(1), 7 | }; 8 | class RawResponse { 9 | /** 10 | * @param {buffer} data 11 | * @param {{[string]: string[]}} headers 12 | * @param {string} statusCode 13 | * @returns {object} response 14 | */ 15 | constructor(data, headers, statusCode) { 16 | this.data = data; 17 | this.headers = headers; 18 | this.statusCode = statusCode; 19 | } 20 | 21 | respond(request, h) { 22 | let payload = this.data; 23 | internals.stubActiveConfig = int2uuid(request.app.themeConfig.variationIndex + 1); 24 | if ( 25 | request.path.startsWith('/checkout.php') || 26 | request.path.startsWith('/finishorder.php') 27 | ) { 28 | payload = this._appendCss(payload.toString('utf8')); 29 | } 30 | // To be removed when we go to Phase 3 31 | if (request.path.startsWith('/checkout')) { 32 | payload = payload 33 | .toString('utf8') 34 | .replace( 35 | /http[s]?:\/\/.*?\/optimized-checkout.css/, 36 | `/stencil/${internals.stubActiveVersion}/${internals.stubActiveConfig}/css/optimized-checkout.css`, 37 | ); 38 | } 39 | const response = h.response(payload).code(this.statusCode); 40 | for (const [name, values] of Object.entries(this.headers)) { 41 | switch (name) { 42 | case 'transfer-encoding': 43 | case 'content-length': 44 | break; 45 | case 'set-cookie': 46 | // Cookies should be an array 47 | response.header('set-cookie', values); 48 | break; 49 | default: 50 | // Other headers should be strings 51 | response.header(name, values.toString()); 52 | } 53 | } 54 | return response; 55 | } 56 | 57 | /** 58 | * @private 59 | * Append checkout.css to override styles. 60 | * @param {string} payload 61 | * @returns {string} 62 | */ 63 | _appendCss(payload) { 64 | const dom = cheerio.load(payload); 65 | const url = `/stencil/${internals.stubActiveVersion}/${internals.stubActiveConfig}/css/checkout.css`; 66 | dom('head').append(``); 67 | return dom.html(); 68 | } 69 | } 70 | export default RawResponse; 71 | -------------------------------------------------------------------------------- /server/plugins/renderer/responses/raw-response.spec.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | import RawResponse from './raw-response.js'; 3 | import { int2uuid } from '../../../lib/utils.js'; 4 | 5 | describe('RawResponse', () => { 6 | const data = Buffer.from('hello'); 7 | const headers = { 8 | 'content-type': 'html/text', 9 | }; 10 | const statusCode = 200; 11 | let request; 12 | let response; 13 | let h; 14 | beforeEach(() => { 15 | request = { 16 | url: {}, 17 | path: '/', 18 | app: { themeConfig: { variationIndex: 1 } }, 19 | }; 20 | response = { 21 | code: () => response, 22 | header: jest.fn(), 23 | }; 24 | h = { 25 | response: jest.fn().mockReturnValue(response), 26 | }; 27 | }); 28 | afterEach(() => { 29 | jest.restoreAllMocks(); 30 | }); 31 | describe('respond()', () => { 32 | it('should respond', () => { 33 | const rawResponse = new RawResponse(data, headers, statusCode); 34 | rawResponse.respond(request, h); 35 | expect(h.response).toHaveBeenCalled(); 36 | }); 37 | it('should append checkout css if is the checkout page', () => { 38 | request.path = '/checkout.php?blah=blah'; 39 | const rawResponse = new RawResponse(data, headers, statusCode); 40 | const id1 = int2uuid(1); 41 | const id2 = int2uuid(2); 42 | const expectedCss = ` { 47 | const rawResponse = new RawResponse(data, headers, statusCode); 48 | rawResponse.respond(request, h); 49 | expect(response.header).not.toHaveBeenCalledWith('transfer-encoding'); 50 | expect(response.header.mock.calls[0]).toContain('content-type'); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /server/plugins/renderer/responses/redirect-response.js: -------------------------------------------------------------------------------- 1 | class RedirectResponse { 2 | /** 3 | * @param {string} location 4 | * @param {{[string]: string[]}} headers 5 | * @param {number} statusCode 6 | */ 7 | constructor(location, headers, statusCode) { 8 | this.location = location; 9 | this.headers = headers; 10 | this.statusCode = statusCode; 11 | } 12 | 13 | respond(request, h) { 14 | const response = h.redirect(this.location).code(this.statusCode); 15 | for (const [name, values] of Object.entries(this.headers)) { 16 | switch (name) { 17 | case 'transfer-encoding': 18 | case 'content-length': 19 | break; 20 | case 'set-cookie': 21 | // Cookies should be an array 22 | response.header( 23 | 'set-cookie', 24 | values.map((val) => 25 | val 26 | .replace(/; Secure/gi, '') 27 | .replace(/(?:;\s)?domain=(?:.+?)(;|$)/gi, ''), 28 | ), 29 | ); 30 | break; 31 | default: 32 | // Other headers should be strings 33 | response.header(name, values.toString()); 34 | } 35 | } 36 | return response; 37 | } 38 | } 39 | export default RedirectResponse; 40 | -------------------------------------------------------------------------------- /server/plugins/router/router.module.spec.js: -------------------------------------------------------------------------------- 1 | import * as Hapi from '@hapi/hapi'; 2 | import * as inert from '@hapi/inert'; 3 | import * as h2o2 from '@hapi/h2o2'; 4 | import router from './router.module'; 5 | 6 | describe('Router', () => { 7 | const SERVER_OPTIONS = { 8 | port: 3000, 9 | }; 10 | const ROUTER_OPTIONS = { 11 | storeUrl: 'https://store-abc124.mybigcommerce.com', 12 | normalStoreUrl: 'http://s1234567890.mybigcommerce.com', 13 | port: SERVER_OPTIONS.port, 14 | }; 15 | const server = new Hapi.Server(SERVER_OPTIONS); 16 | const RendererPluginMock = { 17 | register(_server) { 18 | _server.expose('implementation', (request, h) => h.response('RendererHandlerFired')); 19 | }, 20 | name: 'Renderer', 21 | version: '0.0.1', 22 | }; 23 | const ThemeAssetsMock = { 24 | register(_server) { 25 | _server.expose('cssHandler', (request, h) => h.response('CssHandlerFired')); 26 | _server.expose('assetHandler', (request, h) => h.response('assetHandlerFired')); 27 | }, 28 | name: 'ThemeAssets', 29 | version: '0.0.1', 30 | }; 31 | beforeAll(async () => { 32 | await server.register([ 33 | inert, 34 | h2o2, 35 | RendererPluginMock, 36 | ThemeAssetsMock, 37 | { plugin: router, options: ROUTER_OPTIONS }, 38 | ]); 39 | await server.start(); 40 | }); 41 | afterAll(async () => { 42 | await server.stop(); 43 | }); 44 | it('should call the Renderer handler', async () => { 45 | const options = { 46 | method: 'GET', 47 | url: '/test', 48 | }; 49 | const response = await server.inject(options); 50 | expect(response.statusCode).toEqual(200); 51 | expect(response.payload).toEqual('RendererHandlerFired'); 52 | }); 53 | it('should call the CSS handler', async () => { 54 | const options = { 55 | method: 'GET', 56 | url: '/stencil/123/css/file.css', 57 | }; 58 | const response = await server.inject(options); 59 | expect(response.statusCode).toEqual(200); 60 | expect(response.payload).toEqual('CssHandlerFired'); 61 | }); 62 | it('should call the assets handler', async () => { 63 | const options = { 64 | method: 'GET', 65 | url: '/stencil/123/js/file.js', 66 | }; 67 | const response = await server.inject(options); 68 | expect(response.statusCode).toEqual(200); 69 | expect(response.payload).toEqual('assetHandlerFired'); 70 | }); 71 | it('should inject host and origin headers for GraphQL requests', async () => { 72 | const options = { 73 | method: 'POST', 74 | url: '/graphql', 75 | headers: { authorization: 'auth123' }, 76 | }; 77 | const response = await server.inject(options); 78 | expect(response.request.payload.headers).toMatchObject({ 79 | authorization: 'auth123', 80 | origin: 'https://store-abc124.mybigcommerce.com', 81 | host: 'store-abc124.mybigcommerce.com', 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /server/plugins/theme-assets/theme-assets.module.js: -------------------------------------------------------------------------------- 1 | import { defaultsDeep } from 'lodash-es'; 2 | import * as Boom from '@hapi/boom'; 3 | import path from 'path'; 4 | import { uuidRegExp, uuid2int } from '../../lib/utils.js'; 5 | import cssCompiler from '../../../lib/css/compile.js'; 6 | 7 | const internals = { 8 | options: {}, 9 | }; 10 | function register(server, options) { 11 | internals.options = defaultsDeep(options, internals.options); 12 | server.expose('cssHandler', internals.cssHandler); 13 | server.expose('assetHandler', internals.assetHandler); 14 | } 15 | /** 16 | * Get the variation index from the "ConfigId" in the css filename 17 | * @param {string} fileName 18 | * @returns {number} 19 | */ 20 | internals.getVariationIndex = (fileName) => { 21 | const match = fileName.match(new RegExp(`.+-(${uuidRegExp})$`)); 22 | return match ? uuid2int(match[1]) - 1 : 0; 23 | }; 24 | /** 25 | * Get the original css file name 26 | * @param {string} fileName 27 | * @returns {string} 28 | */ 29 | internals.getOriginalFileName = (fileName) => { 30 | const match = fileName.match(new RegExp(`(.+)-${uuidRegExp}$`)); 31 | return match ? match[1] : fileName; 32 | }; 33 | /** 34 | * CSS Compiler Handler. This utilises the CSS Assembler to gather all of the CSS files and then the 35 | * StencilStyles plugin to compile them all. 36 | * @param request 37 | * @param h 38 | */ 39 | internals.cssHandler = async (request, h) => { 40 | const variationIndex = internals.getVariationIndex(request.params.fileName); 41 | const variationExists = await request.app.themeConfig.variationExists(variationIndex); 42 | if (!variationExists) { 43 | throw Boom.notFound(`Variation ${variationIndex + 1} does not exist.`); 44 | } 45 | // Set the variation to get the right theme configuration 46 | request.app.themeConfig.setVariation(variationIndex); 47 | // Get the theme configuration 48 | const configuration = await request.app.themeConfig.getConfig(); 49 | const fileName = internals.getOriginalFileName(request.params.fileName); 50 | const themeAssetsPath = internals.getThemeAssetsPath(); 51 | try { 52 | const css = await cssCompiler.compile(configuration, themeAssetsPath, fileName); 53 | return h.response(css).type('text/css'); 54 | } catch (err) { 55 | console.error(err); 56 | throw Boom.badData(err); 57 | } 58 | }; 59 | /** 60 | * Assets handler 61 | * 62 | * @param request 63 | * @param h 64 | */ 65 | internals.assetHandler = (request, h) => { 66 | const filePath = path.join(internals.getThemeAssetsPath(), request.params.fileName); 67 | return h.file(filePath); 68 | }; 69 | internals.getThemeAssetsPath = () => { 70 | return path.join(internals.options.themePath, 'assets'); 71 | }; 72 | export const name = 'ThemeAssets'; 73 | export const version = '0.0.1'; 74 | export { register }; 75 | export default { 76 | register, 77 | name, 78 | version, 79 | }; 80 | -------------------------------------------------------------------------------- /test/_mocks/MockWritableStream.js: -------------------------------------------------------------------------------- 1 | import stream from "stream"; 2 | 3 | const { Writable } = stream; 4 | class MockWritableStream extends Writable { 5 | constructor() { 6 | super(); 7 | this.buffer = ''; 8 | } 9 | 10 | _write(chunk, _, next) { 11 | this.buffer += chunk; 12 | next(); 13 | } 14 | 15 | reset() { 16 | this.buffer = ''; 17 | } 18 | } 19 | export default MockWritableStream; 20 | -------------------------------------------------------------------------------- /test/_mocks/api/getConfigurations.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "string", 4 | "variationId": "string", 5 | "storeHash": "string", 6 | "settings": {} 7 | }, 8 | "meta": {} 9 | } 10 | -------------------------------------------------------------------------------- /test/_mocks/api/getVariations.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "string", 4 | "themeName": "string", 5 | "themeId": "string", 6 | "versionId": "string", 7 | "variationName": "string", 8 | "price": 0, 9 | "partner": { 10 | "id": "string", 11 | "name": "string", 12 | "contactUrl": "string", 13 | "contactEmail": "string" 14 | }, 15 | "description": "string", 16 | "industries": [ 17 | "string" 18 | ], 19 | "features": [ 20 | "string" 21 | ], 22 | "optimizedFor": [ 23 | "string" 24 | ], 25 | "screenshot": { 26 | "largePreview": "string", 27 | "largeThumb": "string", 28 | "smallThumb": "string" 29 | }, 30 | "mobileScreenshot": "string", 31 | "demoUrl": "string", 32 | "documentationUrl": "string", 33 | "displayVersion": "string", 34 | "releaseNotes": "string", 35 | "status": "string", 36 | "isCurrent": "boolean", 37 | "relatedVariations": [ 38 | { 39 | "id": "string", 40 | "variationName": "string", 41 | "screenshot": { 42 | "largePreview": "string", 43 | "largeThumb": "string", 44 | "smallThumb": "string", 45 | "isCurrent": "boolean" 46 | }, 47 | "configurationId": "string" 48 | } 49 | ], 50 | "configurationId": "string" 51 | }, 52 | "meta": {} 53 | } 54 | -------------------------------------------------------------------------------- /test/_mocks/api/getVersions.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "string", 4 | "name": "string", 5 | "price": 0, 6 | "displayVersion": "string", 7 | "editorSchema": [ 8 | {} 9 | ], 10 | "status": "string", 11 | "numVariations": 0, 12 | "defaultVariationId": "string", 13 | "screenshot": "string" 14 | }, 15 | "meta": {} 16 | } 17 | -------------------------------------------------------------------------------- /test/_mocks/api/postConfigurations.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "configurationId": "string" 4 | }, 5 | "meta": {} 6 | } 7 | -------------------------------------------------------------------------------- /test/_mocks/build-config/legacy-config/stencil.conf.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Watch options for the core watcher 3 | * @type {{files: string[], ignored: string[]}} 4 | */ 5 | const watchOptions = { 6 | // If files in these directories change, reload the page. 7 | files: [ 8 | '/templates', 9 | '/lang', 10 | ], 11 | 12 | // Do not watch files in these directories 13 | ignored: [ 14 | '/assets/scss', 15 | '/assets/css', 16 | '/assets/dist', 17 | ], 18 | }; 19 | 20 | /** 21 | * Watch any custom files and trigger a rebuild 22 | */ 23 | function development(Bs) { 24 | // Rebuild the bundle once at bootup 25 | setTimeout(() => { 26 | Bs.reload(); 27 | }); 28 | } 29 | 30 | /** 31 | * Hook into the `stencil bundle` command and build your files before they are packaged as a .zip 32 | */ 33 | function production(done) { 34 | // Rebuild the bundle once at bootup 35 | setTimeout(() => { 36 | done(); 37 | }); 38 | } 39 | 40 | module.exports = { watchOptions, development, production }; 41 | -------------------------------------------------------------------------------- /test/_mocks/build-config/noready-config/stencil.conf.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Watch options for the core watcher 3 | * @type {{files: string[], ignored: string[]}} 4 | */ 5 | const watchOptions = { 6 | // If files in these directories change, reload the page. 7 | files: [ 8 | '/templates', 9 | '/lang', 10 | ], 11 | 12 | // Do not watch files in these directories 13 | ignored: [ 14 | '/assets/scss', 15 | '/assets/css', 16 | '/assets/dist', 17 | ], 18 | }; 19 | 20 | /** 21 | * Watch any custom files and trigger a rebuild 22 | */ 23 | function development() { 24 | // Rebuild the bundle once at bootup 25 | setTimeout(() => process.send('reload'), 10); 26 | } 27 | 28 | /** 29 | * Hook into the `stencil bundle` command and build your files before they are packaged as a .zip 30 | */ 31 | function production() { 32 | // Rebuild the bundle once at bootup 33 | setTimeout(() => process.send('done'), 10); 34 | } 35 | 36 | if (process.send) { 37 | // running as a forked worker 38 | process.on('message', message => { 39 | if (message === 'development') { 40 | development(); 41 | } 42 | 43 | if (message === 'production') { 44 | production(); 45 | } 46 | }); 47 | } 48 | 49 | module.exports = { watchOptions }; 50 | -------------------------------------------------------------------------------- /test/_mocks/build-config/noworker-config/stencil.conf.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { }; 2 | -------------------------------------------------------------------------------- /test/_mocks/build-config/valid-config/stencil.conf.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Watch options for the core watcher 3 | * @type {{files: string[], ignored: string[]}} 4 | */ 5 | const watchOptions = { 6 | // If files in these directories change, reload the page. 7 | files: [ 8 | '/templates', 9 | '/lang', 10 | ], 11 | 12 | // Do not watch files in these directories 13 | ignored: [ 14 | '/assets/scss', 15 | '/assets/css', 16 | '/assets/dist', 17 | ], 18 | }; 19 | 20 | /** 21 | * Watch any custom files and trigger a rebuild 22 | */ 23 | function development() { 24 | // Rebuild the bundle once at bootup 25 | setTimeout(() => process.send('reload'), 10); 26 | } 27 | 28 | /** 29 | * Hook into the `stencil bundle` command and build your files before they are packaged as a .zip 30 | */ 31 | function production() { 32 | // Rebuild the bundle once at bootup 33 | setTimeout(() => process.send('done'), 10); 34 | } 35 | 36 | if (process.send) { 37 | // running as a forked worker 38 | process.on('message', message => { 39 | if (message === 'development') { 40 | development(); 41 | } 42 | 43 | if (message === 'production') { 44 | production(); 45 | } 46 | }); 47 | 48 | process.send('ready'); 49 | } 50 | 51 | module.exports = { watchOptions }; 52 | -------------------------------------------------------------------------------- /test/_mocks/frontmatter/absent.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | {{#if products.featured}} 6 | {{> components/products/featured products=products.featured columns=theme_settings.homepage_featured_products_column_count}} 7 | {{/if}} 8 | {{{region name="home_below_featured_products"}}} 9 | 10 | {{#if products.top_sellers}} 11 | {{> components/products/top products=products.top_sellers columns=theme_settings.homepage_top_products_column_count}} 12 | {{/if}} 13 | {{{region name="home_below_top_products"}}} 14 | 15 | {{#if products.new}} 16 | {{> components/products/new products=products.new columns=theme_settings.homepage_new_products_column_count}} 17 | {{/if}} 18 | {{{region name="home_below_new_products"}}} 19 |
-------------------------------------------------------------------------------- /test/_mocks/frontmatter/valid.html: -------------------------------------------------------------------------------- 1 | --- 2 | products: 3 | new: 4 | limit: {{theme_settings.homepage_new_products_count}} 5 | featured: 6 | limit: {{theme_settings.homepage_featured_products_count}} 7 | top_sellers: 8 | limit: {{theme_settings.homepage_top_products_count}} 9 | carousel: {{theme_settings.homepage_show_carousel}} 10 | blog: 11 | recent_posts: 12 | limit: {{theme_settings.homepage_blog_posts_count}} 13 | --- 14 | {{#partial "hero"}} 15 | {{{region name="home_below_menu"}}} 16 | {{#and carousel carousel.slides.length}} 17 | {{> components/carousel arrows=theme_settings.homepage_show_carousel_arrows play_pause_button=theme_settings.homepage_show_carousel_play_pause_button}} 18 | {{/and}} 19 | {{{region name="home_below_carousel"}}} 20 | {{/partial}} 21 | 22 | {{#partial "page"}} 23 | 24 | {{#each shipping_messages}} 25 | {{> components/common/alert/alert-info message}} 26 | {{/each}} 27 | 28 |
29 | {{#if products.featured}} 30 | {{> components/products/featured products=products.featured columns=theme_settings.homepage_featured_products_column_count}} 31 | {{/if}} 32 | {{{region name="home_below_featured_products"}}} 33 | 34 | {{#if products.top_sellers}} 35 | {{> components/products/top products=products.top_sellers columns=theme_settings.homepage_top_products_column_count}} 36 | {{/if}} 37 | {{{region name="home_below_top_products"}}} 38 | 39 | {{#if products.new}} 40 | {{> components/products/new products=products.new columns=theme_settings.homepage_new_products_column_count}} 41 | {{/if}} 42 | {{{region name="home_below_new_products"}}} 43 |
44 | {{/partial}} 45 | {{> layout/base}} 46 | -------------------------------------------------------------------------------- /test/_mocks/malformedSchema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Missing Comma ->" 4 | "settings": [ 5 | { 6 | "type": "string", 7 | "label": "A Title", 8 | "id": "customizable_title" 9 | }, 10 | { 11 | "type": "checkbox", 12 | "label": "Fetch Data", 13 | "id": "front_matter_value" 14 | }, 15 | { 16 | "type": "radio", 17 | "label": "Display That", 18 | "id": "display_that" 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /test/_mocks/themes/bad-schema/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "meta": { 10 | "price": 15000, 11 | "composed_image": "image.jpg" 12 | }, 13 | "settings": { 14 | "color": "#ffffff", 15 | "font": "Sans Something", 16 | "select": "first", 17 | "checkbox": true, 18 | "radio": "first" 19 | }, 20 | "images": { 21 | "logo": { 22 | "width": 100, 23 | "height": 100 24 | }, 25 | "thumb": { 26 | "width": 10, 27 | "height": 10 28 | } 29 | }, 30 | "variations": [ 31 | { 32 | "name": "First" 33 | }, 34 | { 35 | "name": "Second", 36 | "settings": { 37 | "color": "#000000", 38 | "select": "second" 39 | }, 40 | "images": { 41 | "logo": { 42 | "width": 200, 43 | "height": 200 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /test/_mocks/themes/bad-schema/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Missing Comma ->" 4 | "settings": [ 5 | { 6 | "type": "string", 7 | "label": "A Title", 8 | "id": "customizable_title" 9 | }, 10 | { 11 | "type": "checkbox", 12 | "label": "Fetch Data", 13 | "id": "front_matter_value" 14 | }, 15 | { 16 | "type": "radio", 17 | "label": "Display That", 18 | "id": "display_that" 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /test/_mocks/themes/bare-bones/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "variations": [ 5 | { 6 | "name": "First" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/_mocks/themes/component-with-external-template/a.html: -------------------------------------------------------------------------------- 1 | a 2 | 3 | {{> "external/@bigcommerce/theme-ui-components/templates/button"}} 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/component-with-external-template/b.html: -------------------------------------------------------------------------------- 1 | b 2 | 3 | {{> "external/theme-ui-components/templates/button"}} 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "meta": { 10 | "price": 0, 11 | "documentation_url": "https://stencil.bigcommerce.com/docs/what-is-stencil", 12 | "author_name": "BigCommerce", 13 | "author_email": "support@bigcommerce.com", 14 | "author_support_url": "https://www.bigcommerce.com/support", 15 | "composed_image": "composed.jpg", 16 | "features": [ 17 | "fully_responsive" 18 | ] 19 | }, 20 | "settings": { 21 | "color": "#ffffff", 22 | "font": "Sans Something", 23 | "select": "first", 24 | "checkbox": true, 25 | "radio": "first" 26 | }, 27 | "images": { 28 | "logo": { 29 | "width": 100, 30 | "height": 100 31 | }, 32 | "thumb": { 33 | "width": 10, 34 | "height": 10 35 | } 36 | }, 37 | "variations": [ 38 | { 39 | "id": "first", 40 | "name": "First", 41 | "meta": { 42 | "desktop_screenshot": "desktop_light.jpg", 43 | "mobile_screenshot": "mobile_light.jpg", 44 | "description": "First variation", 45 | "demo_url": "https://stencil-first.example.com", 46 | "optimized_for": [ 47 | "arts_crafts" 48 | ], 49 | "industries": [] 50 | }, 51 | "settings": { 52 | "color": "#000000", 53 | "select": "first" 54 | }, 55 | "images": {} 56 | }, 57 | { 58 | "id": "second", 59 | "name": "Second", 60 | "meta": { 61 | "desktop_screenshot": "desktop_bold.jpg", 62 | "mobile_screenshot": "mobile_bold.jpg", 63 | "description": "Second variation", 64 | "demo_url": "https://stencil-second.example.com", 65 | "optimized_for": [ 66 | "arts_crafts" 67 | ], 68 | "industries": [] 69 | }, 70 | "settings": { 71 | "color": "#000000", 72 | "select": "second" 73 | }, 74 | "images": { 75 | "logo": { 76 | "width": 200, 77 | "height": 200 78 | } 79 | } 80 | }, 81 | { 82 | "id": "third", 83 | "name": "Third", 84 | "meta": { 85 | "desktop_screenshot": "desktop_warm.jpg", 86 | "mobile_screenshot": "mobile_warm.jpg", 87 | "description": "Third variation", 88 | "demo_url": "https://stencil-third.example.com", 89 | "optimized_for": [ 90 | "arts_crafts" 91 | ], 92 | "industries": [] 93 | }, 94 | "settings": { 95 | "color": "#FFFFFF", 96 | "select": "third" 97 | }, 98 | "images": { 99 | "logo": { 100 | "width": 150, 101 | "height": 150 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "welcome_back": "Welcome back, {name}" 4 | }, 5 | "footer": { 6 | "brands": "Popular Brands", 7 | "navigate": "Navigate", 8 | "info": "Info", 9 | "categories": "Categories", 10 | "call_us": "Call us at {phone_number}" 11 | }, 12 | "home": { 13 | "heading": "Home" 14 | }, 15 | "blog": { 16 | "recent_posts": "Recent Posts", 17 | "label": "Blog", 18 | "posted_by": "Posted by {name}" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "i18n.Test", 4 | "settings": [ 5 | { 6 | "type": "heading", 7 | "content": "Heading" 8 | }, 9 | { 10 | "type": "paragraph", 11 | "content": "Paragraph" 12 | }, 13 | { 14 | "type": "color", 15 | "label": "Color", 16 | "id": "color" 17 | }, 18 | { 19 | "type": "font", 20 | "label": "Font", 21 | "id": "font", 22 | "options": [ 23 | { 24 | "group": "Karla", 25 | "label": "Karla", 26 | "value": "Google_Karla_400" 27 | }, 28 | { 29 | "group": "Roboto", 30 | "label": "Roboto", 31 | "value": "Google_Roboto_400" 32 | }, 33 | { 34 | "group": "Source Sans Pro", 35 | "label": "Source Sans Pro", 36 | "value": "Google_Source+Sans+Pro_400" 37 | } 38 | ] 39 | }, 40 | { 41 | "type": "select", 42 | "label": "Select", 43 | "id": "select", 44 | "force_reload": true, 45 | "options": [ 46 | { 47 | "value": "first", 48 | "label": "First" 49 | }, 50 | { 51 | "value": "second", 52 | "label": "Second" 53 | }, 54 | { 55 | "value": "third", 56 | "label": "Third" 57 | } 58 | ] 59 | }, 60 | { 61 | "type": "checkbox", 62 | "label": "Checkbox", 63 | "id": "checkbox" 64 | }, 65 | { 66 | "type": "select", 67 | "label": "Select", 68 | "id": "select-two", 69 | "options": [ 70 | { 71 | "value": "first", 72 | "label": "First" 73 | }, 74 | { 75 | "value": "second", 76 | "label": "Second" 77 | } 78 | ] 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "Page", 84 | "settings": [ 85 | { 86 | "type": "paragraph", 87 | "label": "A Title", 88 | "id": "customizable_title", 89 | "content": "hello world" 90 | }, 91 | { 92 | "type": "checkbox", 93 | "label": "Fetch Data", 94 | "id": "front_matter_value" 95 | }, 96 | { 97 | "type": "input", 98 | "label": "Display That", 99 | "id": "display_that" 100 | } 101 | ] 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/schemaTranslations.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n.Test": { 3 | "default": "Test" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/templates/components/a.html: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/templates/components/b.html: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | front_matter_options: 3 | setting_x: {{theme_settings.front_matter_value}}, 4 | --- 5 | 6 | 7 | 8 | page.html 9 | {{head.scripts}} 10 | 11 | 12 | {{#if theme_settings.display_that}} 13 |
That
14 | {{> components/a}} 15 | {{/if}} 16 | {{footer.scripts}} 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-frontmatter/templates/pages/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page2.html 5 | {{head.scripts}} 6 | 7 | 8 |

{{theme_settings.customizable_title}}

9 | {{> components/b}} 10 | {{footer.scripts}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "meta": { 10 | "price": 0, 11 | "documentation_url": "https://stencil.bigcommerce.com/docs/what-is-stencil", 12 | "author_name": "BigCommerce", 13 | "author_email": "support@bigcommerce.com", 14 | "author_support_url": "https://www.bigcommerce.com/support", 15 | "composed_image": "composed.jpg", 16 | "features": [ 17 | "fully_responsive" 18 | ] 19 | }, 20 | "settings": { 21 | "color": "#ffffff", 22 | "font": "Sans Something", 23 | "select": "first", 24 | "checkbox": true, 25 | "radio": "first" 26 | }, 27 | "images": { 28 | "logo": { 29 | "width": 100, 30 | "height": 100 31 | }, 32 | "thumb": { 33 | "width": 10, 34 | "height": 10 35 | } 36 | }, 37 | "variations": [ 38 | { 39 | "id": "first", 40 | "name": "First", 41 | "meta": { 42 | "desktop_screenshot": "desktop_light.jpg", 43 | "mobile_screenshot": "mobile_light.jpg", 44 | "description": "First variation", 45 | "demo_url": "https://stencil-first.example.com", 46 | "optimized_for": [ 47 | "arts_crafts" 48 | ], 49 | "industries": [] 50 | }, 51 | "settings": { 52 | "color": "#000000", 53 | "select": "first" 54 | }, 55 | "images": {} 56 | }, 57 | { 58 | "id": "second", 59 | "name": "Second", 60 | "meta": { 61 | "desktop_screenshot": "desktop_bold.jpg", 62 | "mobile_screenshot": "mobile_bold.jpg", 63 | "description": "Second variation", 64 | "demo_url": "https://stencil-second.example.com", 65 | "optimized_for": [ 66 | "arts_crafts" 67 | ], 68 | "industries": [] 69 | }, 70 | "settings": { 71 | "color": "#000000", 72 | "select": "second" 73 | }, 74 | "images": { 75 | "logo": { 76 | "width": 200, 77 | "height": 200 78 | } 79 | } 80 | }, 81 | { 82 | "id": "third", 83 | "name": "Third", 84 | "meta": { 85 | "desktop_screenshot": "desktop_warm.jpg", 86 | "mobile_screenshot": "mobile_warm.jpg", 87 | "description": "Third variation", 88 | "demo_url": "https://stencil-third.example.com", 89 | "optimized_for": [ 90 | "arts_crafts" 91 | ], 92 | "industries": [] 93 | }, 94 | "settings": { 95 | "color": "#FFFFFF", 96 | "select": "third" 97 | }, 98 | "images": { 99 | "logo": { 100 | "width": 150, 101 | "height": 150 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/composed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/composed.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/desktop_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/desktop_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/desktop_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/desktop_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/desktop_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/desktop_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/mobile_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/mobile_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/mobile_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/mobile_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/meta/mobile_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-schema/meta/mobile_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-schema/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Test", 4 | "settings": [ 5 | { 6 | "type": "heading" 7 | }, 8 | { 9 | "type": "paragraph", 10 | "content": "Paragraph" 11 | }, 12 | { 13 | "type": "color", 14 | "label": "Color", 15 | "id": "color" 16 | }, 17 | { 18 | "type": "select", 19 | "label": "Select", 20 | "id": "select", 21 | "force_reload": true, 22 | "options": [ 23 | { 24 | "value": "first", 25 | "label": "First" 26 | }, 27 | { 28 | "value": "second", 29 | "label": "Second" 30 | }, 31 | { 32 | "value": "third", 33 | "label": "Third" 34 | } 35 | ] 36 | }, 37 | { 38 | "type": "checkbox", 39 | "label": "Checkbox", 40 | "id": "checkbox" 41 | }, 42 | { 43 | "type": "select", 44 | "label": "Select", 45 | "id": "select-two", 46 | "options": [ 47 | { 48 | "value": "first", 49 | "label": "First" 50 | }, 51 | { 52 | "value": "second", 53 | "label": "Second" 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | { 60 | "name": "Page", 61 | "settings": [ 62 | { 63 | "type": "paragraph", 64 | "label": "A Title", 65 | "id": "customizable_title", 66 | "content": "hello world" 67 | }, 68 | { 69 | "type": "checkbox", 70 | "label": "Fetch Data", 71 | "id": "front_matter_value" 72 | }, 73 | { 74 | "type": "input", 75 | "label": "Display That", 76 | "id": "display_that" 77 | } 78 | ] 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/assets/scss/test.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #000000; 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/assets/scss/theme.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #0000AA; 3 | } 4 | 5 | $font: "Arial"; 6 | 7 | @if not contains($font, "'Clear Sans', sans-serif") { 8 | @import "test"; 9 | } 10 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "meta": { 10 | "price": 0, 11 | "documentation_url": "https://stencil.bigcommerce.com/docs/what-is-stencil", 12 | "author_name": "BigCommerce", 13 | "author_email": "support@bigcommerce.com", 14 | "author_support_url": "https://www.bigcommerce.com/support", 15 | "composed_image": "composed.jpg", 16 | "features": [ 17 | "fully_responsive" 18 | ] 19 | }, 20 | "settings": { 21 | "color": "#ffffff", 22 | "font": "Sans Something", 23 | "select": "first", 24 | "checkbox": true, 25 | "radio": "first" 26 | }, 27 | "images": { 28 | "logo": { 29 | "width": 100, 30 | "height": 100 31 | }, 32 | "thumb": { 33 | "width": 10, 34 | "height": 10 35 | } 36 | }, 37 | "variations": [ 38 | { 39 | "id": "first", 40 | "name": "First", 41 | "meta": { 42 | "desktop_screenshot": "desktop_light.jpg", 43 | "mobile_screenshot": "mobile_light.jpg", 44 | "description": "First variation", 45 | "demo_url": "https://stencil-first.example.com", 46 | "optimized_for": [ 47 | "arts_crafts" 48 | ], 49 | "industries": [] 50 | }, 51 | "settings": { 52 | "color": "#000000", 53 | "select": "first" 54 | }, 55 | "images": {} 56 | }, 57 | { 58 | "id": "second", 59 | "name": "Second", 60 | "meta": { 61 | "desktop_screenshot": "desktop_bold.jpg", 62 | "mobile_screenshot": "mobile_bold.jpg", 63 | "description": "Second variation", 64 | "demo_url": "https://stencil-second.example.com", 65 | "optimized_for": [ 66 | "arts_crafts" 67 | ], 68 | "industries": [] 69 | }, 70 | "settings": { 71 | "color": "#000000", 72 | "select": "second" 73 | }, 74 | "images": { 75 | "logo": { 76 | "width": 200, 77 | "height": 200 78 | } 79 | } 80 | }, 81 | { 82 | "id": "third", 83 | "name": "Third", 84 | "meta": { 85 | "desktop_screenshot": "desktop_warm.jpg", 86 | "mobile_screenshot": "mobile_warm.jpg", 87 | "description": "Third variation", 88 | "demo_url": "https://stencil-third.example.com", 89 | "optimized_for": [ 90 | "arts_crafts" 91 | ], 92 | "industries": [] 93 | }, 94 | "settings": { 95 | "color": "#FFFFFF", 96 | "select": "third" 97 | }, 98 | "images": { 99 | "logo": { 100 | "width": 150, 101 | "height": 150 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/config.stencil.json: -------------------------------------------------------------------------------- 1 | { 2 | "customLayouts": { 3 | "brand": {}, 4 | "category": {}, 5 | "page": {}, 6 | "product": {} 7 | }, 8 | "normalStoreUrl": "https://url-from-answers.mybigcommerce.com", 9 | "port": 3003 10 | } -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "welcome_back": "Welcome back, {name}" 4 | }, 5 | "footer": { 6 | "brands": "Popular Brands", 7 | "navigate": "Navigate", 8 | "info": "Info", 9 | "categories": "Categories", 10 | "call_us": "Call us at {phone_number}" 11 | }, 12 | "home": { 13 | "heading": "Home" 14 | }, 15 | "blog": { 16 | "recent_posts": "Recent Posts", 17 | "label": "Blog", 18 | "posted_by": "Posted by {name}" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/composed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/composed.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/desktop_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/desktop_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/desktop_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/desktop_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/desktop_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/desktop_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/mobile_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/mobile_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/mobile_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/mobile_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/mobile_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/meta/mobile_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/mock-theme.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/mock-theme.zip -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "i18n.Test", 4 | "settings": [ 5 | { 6 | "type": "heading", 7 | "content": "Heading" 8 | }, 9 | { 10 | "type": "paragraph", 11 | "content": "Paragraph" 12 | }, 13 | { 14 | "type": "color", 15 | "label": "Color", 16 | "id": "color" 17 | }, 18 | { 19 | "type": "font", 20 | "label": "Font", 21 | "id": "font", 22 | "options": [ 23 | { 24 | "group": "Karla", 25 | "label": "Karla", 26 | "value": "Google_Karla_400" 27 | }, 28 | { 29 | "group": "Roboto", 30 | "label": "Roboto", 31 | "value": "Google_Roboto_400" 32 | }, 33 | { 34 | "group": "Source Sans Pro", 35 | "label": "Source Sans Pro", 36 | "value": "Google_Source+Sans+Pro_400" 37 | } 38 | ] 39 | }, 40 | { 41 | "type": "select", 42 | "label": "Select", 43 | "id": "select", 44 | "force_reload": true, 45 | "options": [ 46 | { 47 | "value": "first", 48 | "label": "First" 49 | }, 50 | { 51 | "value": "second", 52 | "label": "Second" 53 | }, 54 | { 55 | "value": "third", 56 | "label": "Third" 57 | } 58 | ] 59 | }, 60 | { 61 | "type": "checkbox", 62 | "label": "Checkbox", 63 | "id": "checkbox" 64 | }, 65 | { 66 | "type": "select", 67 | "label": "Select", 68 | "id": "select-two", 69 | "options": [ 70 | { 71 | "value": "first", 72 | "label": "First" 73 | }, 74 | { 75 | "value": "second", 76 | "label": "Second" 77 | } 78 | ] 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "Page", 84 | "settings": [ 85 | { 86 | "type": "paragraph", 87 | "label": "A Title", 88 | "id": "customizable_title", 89 | "content": "hello world" 90 | }, 91 | { 92 | "type": "checkbox", 93 | "label": "Fetch Data", 94 | "id": "front_matter_value" 95 | }, 96 | { 97 | "type": "input", 98 | "label": "Display That", 99 | "id": "display_that" 100 | } 101 | ] 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/schemaTranslations.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n.Test": { 3 | "default": "Test" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/secrets.stencil.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessToken": "accessToken_from_answers" 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/templates/components/a.html: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/templates/components/b.html: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | front_matter_options: 3 | setting_x: {{theme_settings.front_matter_value}} 4 | --- 5 | 6 | 7 | 8 | page.html 9 | {{head.scripts}} 10 | {{ stylesheet 'assets/css/theme.css' }} 11 | 12 | 13 | {{#if theme_settings.display_that}} 14 |
That
15 | {{> components/a}} 16 | {{/if}} 17 | {{footer.scripts}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass-to-fix/templates/pages/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page2.html 5 | {{head.scripts}} 6 | 7 | 8 |

{{theme_settings.customizable_title}}

9 | {{> components/b}} 10 | {{footer.scripts}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/assets/scss/test.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #000000; 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/assets/scss/theme.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #0000AA; 3 | } 4 | 5 | $font: "Arial"; 6 | 7 | @if not contains($font, "'Clear Sans', sans-serif") { 8 | @import "test"; 9 | } 10 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "meta": { 10 | "price": 0, 11 | "documentation_url": "https://stencil.bigcommerce.com/docs/what-is-stencil", 12 | "author_name": "BigCommerce", 13 | "author_email": "support@bigcommerce.com", 14 | "author_support_url": "https://www.bigcommerce.com/support", 15 | "composed_image": "composed.jpg", 16 | "features": [ 17 | "fully_responsive" 18 | ] 19 | }, 20 | "settings": { 21 | "color": "#ffffff", 22 | "font": "Sans Something", 23 | "select": "first", 24 | "checkbox": true, 25 | "radio": "first" 26 | }, 27 | "images": { 28 | "logo": { 29 | "width": 100, 30 | "height": 100 31 | }, 32 | "thumb": { 33 | "width": 10, 34 | "height": 10 35 | } 36 | }, 37 | "variations": [ 38 | { 39 | "id": "first", 40 | "name": "First", 41 | "meta": { 42 | "desktop_screenshot": "desktop_light.jpg", 43 | "mobile_screenshot": "mobile_light.jpg", 44 | "description": "First variation", 45 | "demo_url": "https://stencil-first.example.com", 46 | "optimized_for": [ 47 | "arts_crafts" 48 | ], 49 | "industries": [] 50 | }, 51 | "settings": { 52 | "color": "#000000", 53 | "select": "first" 54 | }, 55 | "images": {} 56 | }, 57 | { 58 | "id": "second", 59 | "name": "Second", 60 | "meta": { 61 | "desktop_screenshot": "desktop_bold.jpg", 62 | "mobile_screenshot": "mobile_bold.jpg", 63 | "description": "Second variation", 64 | "demo_url": "https://stencil-second.example.com", 65 | "optimized_for": [ 66 | "arts_crafts" 67 | ], 68 | "industries": [] 69 | }, 70 | "settings": { 71 | "color": "#000000", 72 | "select": "second" 73 | }, 74 | "images": { 75 | "logo": { 76 | "width": 200, 77 | "height": 200 78 | } 79 | } 80 | }, 81 | { 82 | "id": "third", 83 | "name": "Third", 84 | "meta": { 85 | "desktop_screenshot": "desktop_warm.jpg", 86 | "mobile_screenshot": "mobile_warm.jpg", 87 | "description": "Third variation", 88 | "demo_url": "https://stencil-third.example.com", 89 | "optimized_for": [ 90 | "arts_crafts" 91 | ], 92 | "industries": [] 93 | }, 94 | "settings": { 95 | "color": "#FFFFFF", 96 | "select": "third" 97 | }, 98 | "images": { 99 | "logo": { 100 | "width": 150, 101 | "height": 150 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/config.stencil.json: -------------------------------------------------------------------------------- 1 | { 2 | "customLayouts": { 3 | "brand": {}, 4 | "category": {}, 5 | "page": {}, 6 | "product": {} 7 | }, 8 | "normalStoreUrl": "https://url-from-answers.mybigcommerce.com", 9 | "port": 3003 10 | } -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "welcome_back": "Welcome back, {name}" 4 | }, 5 | "footer": { 6 | "brands": "Popular Brands", 7 | "navigate": "Navigate", 8 | "info": "Info", 9 | "categories": "Categories", 10 | "call_us": "Call us at {phone_number}" 11 | }, 12 | "home": { 13 | "heading": "Home" 14 | }, 15 | "blog": { 16 | "recent_posts": "Recent Posts", 17 | "label": "Blog", 18 | "posted_by": "Posted by {name}" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/composed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/composed.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/desktop_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/desktop_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/desktop_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/desktop_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/desktop_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/desktop_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/mobile_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/mobile_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/mobile_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/mobile_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/meta/mobile_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/meta/mobile_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/mock-theme.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/invalid-scss-latest-node-sass/mock-theme.zip -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "i18n.Test", 4 | "settings": [ 5 | { 6 | "type": "heading", 7 | "content": "Heading" 8 | }, 9 | { 10 | "type": "paragraph", 11 | "content": "Paragraph" 12 | }, 13 | { 14 | "type": "color", 15 | "label": "Color", 16 | "id": "color" 17 | }, 18 | { 19 | "type": "font", 20 | "label": "Font", 21 | "id": "font", 22 | "options": [ 23 | { 24 | "group": "Karla", 25 | "label": "Karla", 26 | "value": "Google_Karla_400" 27 | }, 28 | { 29 | "group": "Roboto", 30 | "label": "Roboto", 31 | "value": "Google_Roboto_400" 32 | }, 33 | { 34 | "group": "Source Sans Pro", 35 | "label": "Source Sans Pro", 36 | "value": "Google_Source+Sans+Pro_400" 37 | } 38 | ] 39 | }, 40 | { 41 | "type": "select", 42 | "label": "Select", 43 | "id": "select", 44 | "force_reload": true, 45 | "options": [ 46 | { 47 | "value": "first", 48 | "label": "First" 49 | }, 50 | { 51 | "value": "second", 52 | "label": "Second" 53 | }, 54 | { 55 | "value": "third", 56 | "label": "Third" 57 | } 58 | ] 59 | }, 60 | { 61 | "type": "checkbox", 62 | "label": "Checkbox", 63 | "id": "checkbox" 64 | }, 65 | { 66 | "type": "select", 67 | "label": "Select", 68 | "id": "select-two", 69 | "options": [ 70 | { 71 | "value": "first", 72 | "label": "First" 73 | }, 74 | { 75 | "value": "second", 76 | "label": "Second" 77 | } 78 | ] 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "Page", 84 | "settings": [ 85 | { 86 | "type": "paragraph", 87 | "label": "A Title", 88 | "id": "customizable_title", 89 | "content": "hello world" 90 | }, 91 | { 92 | "type": "checkbox", 93 | "label": "Fetch Data", 94 | "id": "front_matter_value" 95 | }, 96 | { 97 | "type": "input", 98 | "label": "Display That", 99 | "id": "display_that" 100 | } 101 | ] 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/schemaTranslations.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n.Test": { 3 | "default": "Test" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/secrets.stencil.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessToken": "accessToken_from_answers" 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/templates/components/a.html: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/templates/components/b.html: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | front_matter_options: 3 | setting_x: {{theme_settings.front_matter_value}} 4 | --- 5 | 6 | 7 | 8 | page.html 9 | {{head.scripts}} 10 | {{ stylesheet 'assets/css/theme.css' }} 11 | 12 | 13 | {{#if theme_settings.display_that}} 14 |
That
15 | {{> components/a}} 16 | {{/if}} 17 | {{footer.scripts}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-scss-latest-node-sass/templates/pages/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page2.html 5 | {{head.scripts}} 6 | 7 | 8 |

{{theme_settings.customizable_title}}

9 | {{> components/b}} 10 | {{footer.scripts}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-translations/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "welcome_back": "Welcome back, {name}" 4 | }, 5 | "footer": { 6 | "brands": "Popular Brands", 7 | "navigate": "Navigate", 8 | "info": "Info", 9 | "categories": "Categories", 10 | "call_us": "Call us at {phone_number}" 11 | }, 12 | "home": { 13 | "heading": "Home" 14 | }, 15 | "blog": { 16 | "recent_posts": "Recent Posts", 17 | "label": "Blog", 18 | "posted_by": "Posted by {name}" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-translations/templates/components/a.html: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-translations/templates/components/b.html: -------------------------------------------------------------------------------- 1 | b 2 | 3 | 4 | 6 | 7 | more text here -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-translations/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page.html 5 | 6 | 7 | {{ lang 'failed' }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/_mocks/themes/invalid-translations/templates/pages/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page2.html 5 | {{head.scripts}} 6 | 7 | 8 |

{{theme_settings.customizable_title}}

9 | {{> components/b}} 10 | {{footer.scripts}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/_mocks/themes/missing-variation/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "settings": { 10 | "color": "#ffffff", 11 | "font": "Sans Something", 12 | "select": "first", 13 | "checkbox": true, 14 | "radio": "first" 15 | }, 16 | "images": { 17 | "logo": { 18 | "width": 100, 19 | "height": 100 20 | }, 21 | "thumb": { 22 | "width": 10, 23 | "height": 10 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/bottom.html: -------------------------------------------------------------------------------- 1 |
2 | {{#block "bottom"}} {{/block}} 3 |
4 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/dynamic/a.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{{region name='dynamic_a'}}} 3 |
  • 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/dynamic/b.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{{region name='dynamic_b'}}} 3 |
  • 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/dynamic/c.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{{region name='dynamic_c'}}} 3 |
  • 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/middle.html: -------------------------------------------------------------------------------- 1 |
    2 | {{#block "middle"}} {{/block}} 3 |
    4 | {{> components/other}} 5 |
    6 |
    7 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/other.html: -------------------------------------------------------------------------------- 1 |
      2 |
    • 3 |

      {{{region name="other"}}}

      4 |
    • 5 |
    6 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/components/top.html: -------------------------------------------------------------------------------- 1 |
    2 | {{#block "top"}} {{/block}} 3 |
      4 | {{#each something.something}} 5 | {{dynamicComponent 'components/dynamic'}} 6 | {{/each}} 7 |
    8 |
    9 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/layout/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{{head.scripts}}} 4 | 5 | 6 | {{> components/top}} 7 | {{> components/middle}} 8 | {{> components/bottom}} 9 | 10 | {{{footer.scripts}}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/_mocks/themes/regions/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | {{#partial "bottom"}} 2 |
    Bottom Section
    3 | {{{region name='bottom_region'}}} 4 | {{/partial}} 5 | 6 | {{#partial "top"}}
    Top Section
    {{{region name='top_region'}}} {{/partial}} 7 | 8 | {{#partial "middle"}} 9 |
    Middle Section
    10 | {{{region name='middle_region'}}} 11 | {{/partial}} 12 | 13 | {{> layout/base}} 14 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/assets/custom/css/test.css: -------------------------------------------------------------------------------- 1 | .test { 2 | color: red; 3 | } -------------------------------------------------------------------------------- /test/_mocks/themes/valid/assets/scss/checkout.scss: -------------------------------------------------------------------------------- 1 | h2 { 2 | color: #0000AA; 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/assets/scss/theme.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: #0000AA; 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stencil", 3 | "version": "1.0", 4 | "css_compiler": "scss", 5 | "autoprefixer_cascade": true, 6 | "autoprefixer_browsers": [ 7 | "> 5% in US" 8 | ], 9 | "meta": { 10 | "price": 0, 11 | "documentation_url": "https://stencil.bigcommerce.com/docs/what-is-stencil", 12 | "author_name": "BigCommerce", 13 | "author_email": "support@bigcommerce.com", 14 | "author_support_url": "https://www.bigcommerce.com/support", 15 | "composed_image": "composed.jpg", 16 | "features": [ 17 | "fully_responsive" 18 | ] 19 | }, 20 | "settings": { 21 | "color": "#ffffff", 22 | "font": "Sans Something", 23 | "select": "first", 24 | "checkbox": true, 25 | "radio": "first" 26 | }, 27 | "images": { 28 | "logo": { 29 | "width": 100, 30 | "height": 100 31 | }, 32 | "thumb": { 33 | "width": 10, 34 | "height": 10 35 | } 36 | }, 37 | "variations": [ 38 | { 39 | "id": "first", 40 | "name": "First", 41 | "meta": { 42 | "desktop_screenshot": "desktop_light.jpg", 43 | "mobile_screenshot": "mobile_light.jpg", 44 | "description": "First variation", 45 | "demo_url": "https://stencil-first.example.com", 46 | "optimized_for": [ 47 | "arts_crafts" 48 | ], 49 | "industries": [] 50 | }, 51 | "settings": { 52 | "color": "#000000", 53 | "select": "first" 54 | }, 55 | "images": {} 56 | }, 57 | { 58 | "id": "second", 59 | "name": "Second", 60 | "meta": { 61 | "desktop_screenshot": "desktop_bold.jpg", 62 | "mobile_screenshot": "mobile_bold.jpg", 63 | "description": "Second variation", 64 | "demo_url": "https://stencil-second.example.com", 65 | "optimized_for": [ 66 | "arts_crafts" 67 | ], 68 | "industries": [] 69 | }, 70 | "settings": { 71 | "color": "#000000", 72 | "select": "second" 73 | }, 74 | "images": { 75 | "logo": { 76 | "width": 200, 77 | "height": 200 78 | } 79 | } 80 | }, 81 | { 82 | "id": "third", 83 | "name": "Third", 84 | "meta": { 85 | "desktop_screenshot": "desktop_warm.jpg", 86 | "mobile_screenshot": "mobile_warm.jpg", 87 | "description": "Third variation", 88 | "demo_url": "https://stencil-third.example.com", 89 | "optimized_for": [ 90 | "arts_crafts" 91 | ], 92 | "industries": [] 93 | }, 94 | "settings": { 95 | "color": "#FFFFFF", 96 | "select": "third" 97 | }, 98 | "images": { 99 | "logo": { 100 | "width": 150, 101 | "height": 150 102 | } 103 | } 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/config.stencil.json: -------------------------------------------------------------------------------- 1 | { 2 | "customLayouts": { 3 | "brand": {}, 4 | "category": {}, 5 | "page": {}, 6 | "product": {} 7 | }, 8 | "normalStoreUrl": "https://url-from-answers.mybigcommerce.com", 9 | "port": 3003 10 | } -------------------------------------------------------------------------------- /test/_mocks/themes/valid/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "welcome_back": "Welcome back, {name}" 4 | }, 5 | "footer": { 6 | "brands": "Popular Brands", 7 | "navigate": "Navigate", 8 | "info": "Info", 9 | "categories": "Categories", 10 | "call_us": "Call us at {phone_number}" 11 | }, 12 | "home": { 13 | "heading": "Home" 14 | }, 15 | "blog": { 16 | "recent_posts": "Recent Posts", 17 | "label": "Blog", 18 | "posted_by": "Posted by {name}" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/composed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/composed.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/desktop_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/desktop_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/desktop_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/desktop_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/desktop_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/desktop_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/mobile_bold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/mobile_bold.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/mobile_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/mobile_light.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/meta/mobile_warm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/meta/mobile_warm.jpg -------------------------------------------------------------------------------- /test/_mocks/themes/valid/mock-theme.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/_mocks/themes/valid/mock-theme.zip -------------------------------------------------------------------------------- /test/_mocks/themes/valid/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "i18n.Test", 4 | "settings": [ 5 | { 6 | "type": "heading", 7 | "content": "Heading" 8 | }, 9 | { 10 | "type": "paragraph", 11 | "content": "Paragraph" 12 | }, 13 | { 14 | "type": "color", 15 | "label": "Color", 16 | "id": "color" 17 | }, 18 | { 19 | "type": "font", 20 | "label": "Font", 21 | "id": "font", 22 | "options": [ 23 | { 24 | "group": "Karla", 25 | "label": "Karla", 26 | "value": "Google_Karla_400" 27 | }, 28 | { 29 | "group": "Roboto", 30 | "label": "Roboto", 31 | "value": "Google_Roboto_400" 32 | }, 33 | { 34 | "group": "Source Sans Pro", 35 | "label": "Source Sans Pro", 36 | "value": "Google_Source+Sans+Pro_400" 37 | } 38 | ] 39 | }, 40 | { 41 | "type": "select", 42 | "label": "Select", 43 | "id": "select", 44 | "force_reload": true, 45 | "options": [ 46 | { 47 | "value": "first", 48 | "label": "First" 49 | }, 50 | { 51 | "value": "second", 52 | "label": "Second" 53 | }, 54 | { 55 | "value": "third", 56 | "label": "Third" 57 | } 58 | ] 59 | }, 60 | { 61 | "type": "checkbox", 62 | "label": "Checkbox", 63 | "id": "checkbox" 64 | }, 65 | { 66 | "type": "select", 67 | "label": "Select", 68 | "id": "select-two", 69 | "options": [ 70 | { 71 | "value": "first", 72 | "label": "First" 73 | }, 74 | { 75 | "value": "second", 76 | "label": "Second" 77 | } 78 | ] 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "Page", 84 | "settings": [ 85 | { 86 | "type": "paragraph", 87 | "label": "A Title", 88 | "id": "customizable_title", 89 | "content": "hello world" 90 | }, 91 | { 92 | "type": "checkbox", 93 | "label": "Fetch Data", 94 | "id": "front_matter_value" 95 | }, 96 | { 97 | "type": "input", 98 | "label": "Display That", 99 | "id": "display_that" 100 | } 101 | ] 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/schemaTranslations.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n.Test": { 3 | "default": "Test" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/secrets.stencil.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessToken": "accessToken_from_answers" 3 | } 4 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/components/a.html: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/components/b.html: -------------------------------------------------------------------------------- 1 | b 2 | 3 | 4 | 6 | 7 | more text here -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/components/c.html: -------------------------------------------------------------------------------- 1 | Here is the list: 2 | {{#> components/ul }} 3 | {{> components/li text="Item 1" link="item_link_1"}} 4 | {{> components/li text="Item 2" link="item_link_2"}} 5 | {{> components/li text="Item 3" link="item_link_3"}} 6 | {{/components/ul}} -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/components/li.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{text}} 3 |
  • -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/components/ul.html: -------------------------------------------------------------------------------- 1 |
      2 | {{> @partial-block }} 3 |
        -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/pages/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | front_matter_options: 3 | setting_x: {{theme_settings.front_matter_value}} 4 | --- 5 | 6 | 7 | 8 | page.html 9 | {{head.scripts}} 10 | {{{stylesheet '/assets/css/theme.css'}}} 11 | {{{stylesheet '/assets/css/checkout.css'}}} 12 | 13 | 14 | {{#if theme_settings.display_that}} 15 |
        That
        16 | {{> components/a}} 17 | {{/if}} 18 | {{footer.scripts}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/pages/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page2.html 5 | {{head.scripts}} 6 | 7 | 8 |

        {{theme_settings.customizable_title}}

        9 | {{> components/b}} 10 | {{footer.scripts}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/_mocks/themes/valid/templates/pages/page3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | page3.html 5 | {{head.scripts}} 6 | 7 | 8 |

        {{theme_settings.customizable_title}}

        9 | {{> components/c }} 10 | 11 | -------------------------------------------------------------------------------- /test/assertions/assertNoMutations.js: -------------------------------------------------------------------------------- 1 | /** Asserts that all the passed entities weren't mutated after executing the passed procedure 2 | * 3 | * @param {object[]} entities 4 | * @param {Function} procedure 5 | * @returns {Promise} 6 | */ 7 | export async function assertNoMutations(entities, procedure) { 8 | const entitiesBefore = entities.map((entity) => JSON.stringify(entity)); 9 | 10 | await procedure(); 11 | 12 | entities.forEach((entity, i) => { 13 | expect(entitiesBefore[i]).toEqual(JSON.stringify(entity)); 14 | }); 15 | } 16 | 17 | export default { assertNoMutations }; 18 | -------------------------------------------------------------------------------- /test/assets/cat_and_dog.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigcommerce/stencil-cli/a32a36b4d1343b7d7683ec02d637a941e80f8658/test/assets/cat_and_dog.jpeg --------------------------------------------------------------------------------