@emulsify/core 0.0.0-development

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.cli/init.js +168 -0
  2. package/.editorconfig +5 -0
  3. package/.eslintignore +2 -0
  4. package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +18 -0
  5. package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +11 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +19 -0
  7. package/.github/dependabot.yml +6 -0
  8. package/.github/workflows/addtoprojects.yml +21 -0
  9. package/.github/workflows/contributors.yml +23 -0
  10. package/.github/workflows/lint.yml +22 -0
  11. package/.github/workflows/semantic-release.yml +24 -0
  12. package/.history/.releaserc_20240607133550 +11 -0
  13. package/.history/.releaserc_20240607134831 +18 -0
  14. package/.history/.releaserc_20240607135005 +11 -0
  15. package/.history/package_20240607132936.json +121 -0
  16. package/.history/package_20240607135135.json +121 -0
  17. package/.history/package_20240607135150.json +121 -0
  18. package/.history/package_20240607135242.json +124 -0
  19. package/.history/package_20240607135251.json +124 -0
  20. package/.history/package_20240607135337.json +127 -0
  21. package/.history/package_20240607145546.json +135 -0
  22. package/.husky/commit-msg +4 -0
  23. package/.husky/pre-commit +4 -0
  24. package/.nvmrc +1 -0
  25. package/.prettierignore +4 -0
  26. package/.storybook/_drupal.js +27 -0
  27. package/.storybook/emulsifyTheme.js +38 -0
  28. package/.storybook/main.js +22 -0
  29. package/.storybook/manager-head.html +122 -0
  30. package/.storybook/manager.js +15 -0
  31. package/.storybook/preview.js +40 -0
  32. package/.storybook/setupTwig.js +59 -0
  33. package/.storybook/setupTwig.test.js +33 -0
  34. package/.storybook/webpack.config.js +67 -0
  35. package/CODE_OF_CONDUCT.md +56 -0
  36. package/LICENSE +674 -0
  37. package/README.md +72 -0
  38. package/assets/images/corner-bkg.png +0 -0
  39. package/assets/images/emulsify-logo-sb.svg +8 -0
  40. package/assets/images/logo.png +0 -0
  41. package/commitlint.config.js +3 -0
  42. package/config/.prettierrc.json +4 -0
  43. package/config/.stylelintrc.json +61 -0
  44. package/config/a11y.config.js +61 -0
  45. package/config/babel.config.js +18 -0
  46. package/config/eslintrc.config.json +68 -0
  47. package/config/jest.config.js +19 -0
  48. package/config/postcss.config.js +5 -0
  49. package/config/webpack/app.js +1 -0
  50. package/config/webpack/css/style.js +1 -0
  51. package/config/webpack/css.js +1 -0
  52. package/config/webpack/loaders.js +87 -0
  53. package/config/webpack/plugins.js +48 -0
  54. package/config/webpack/svgSprite.js +5 -0
  55. package/config/webpack/webpack.common.js +72 -0
  56. package/config/webpack/webpack.dev.js +7 -0
  57. package/config/webpack/webpack.prod.js +6 -0
  58. package/package.json +136 -0
  59. package/release.config.js +11 -0
  60. package/scripts/a11y.js +92 -0
  61. package/scripts/a11y.test.js +159 -0
  62. package/scripts/loadYaml.js +17 -0
  63. package/scripts/loadYaml.test.js +30 -0
package/package.json ADDED
@@ -0,0 +1,136 @@
1
+ {
2
+ "name": "@emulsify/core",
3
+ "version": "0.0.0-development",
4
+ "description": "Bundled tooling for Storybook development + Webpack Build",
5
+ "keywords": [
6
+ "component library",
7
+ "design system",
8
+ "drupal",
9
+ "pattern library",
10
+ "storybook",
11
+ "styleguide"
12
+ ],
13
+ "author": "Four Kitchens <shout@fourkitchens.com>",
14
+ "license": "GPL-2.0",
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/emulsify-ds/emulsify-core.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/emulsify-ds/emulsify-core/issues"
24
+ },
25
+ "homepage": "https://github.com/emulsify-ds/emulsify-core#readme",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "coverage": "npm run test && open-cli .coverage/lcov-report/index.html",
31
+ "format": "npm run lint-fix; npm run prettier-fix",
32
+ "husky:commit-msg": "commitlint --edit $1",
33
+ "husky:pre-commit": "npm run lint",
34
+ "lint": "npm run lint-js",
35
+ "lint-fix": "npm run lint-js -- --fix",
36
+ "lint-js": "eslint --config config/eslintrc.config.json --no-error-on-unmatched-pattern ./config ./storybook",
37
+ "lint-staged": "lint-staged",
38
+ "prepare": "[ -d '.git' ] && (husky install) || true",
39
+ "prettier": "prettier --config config/prettierrc.json --ignore-unknown \"**/*.{js,yml,scss,md}\"",
40
+ "prettier-fix": "prettier --config config/prettierrc.json --write --ignore-unknown \"**/*.{js,yml,scss,md}\"",
41
+ "semantic-release": "semantic-release",
42
+ "storybook": "storybook dev --ci -s ../../dist,../../assets/images,../../assets/icons,../../assets/videos -p 6006",
43
+ "storybook-build": "storybook build -s ../../dist,../../assets/images,../../assets/icons,../../assets/videos -o .out",
44
+ "storybook-deploy": "storybook-to-ghpages -o .out",
45
+ "test": "jest --coverage --config ./config/jest.config.js",
46
+ "twatch": "jest --no-coverage --watch --verbose"
47
+ },
48
+ "dependencies": {
49
+ "@babel/core": "^7.24.0",
50
+ "@babel/eslint-parser": "^7.23.10",
51
+ "@emulsify/cli": "^1.6.0",
52
+ "@storybook/addon-a11y": "^7.6.17",
53
+ "@storybook/addon-actions": "^7.6.17",
54
+ "@storybook/addon-essentials": "^7.6.17",
55
+ "@storybook/addon-links": "^7.6.17",
56
+ "@storybook/addon-styling-webpack": "^1.0.0",
57
+ "@storybook/addon-themes": "^7.6.17",
58
+ "@storybook/html": "^7.6.17",
59
+ "@storybook/html-webpack5": "^7.6.17",
60
+ "add-attributes-twig-extension": "^0.1.0",
61
+ "autoprefixer": "^10.4.19",
62
+ "babel-loader": "^9.1.3",
63
+ "babel-preset-minify": "^0.5.2",
64
+ "bem-twig-extension": "^0.1.1",
65
+ "breakpoint-sass": "^3.0.0",
66
+ "chalk": "^5.2.0",
67
+ "clean-webpack-plugin": "^4.0.0",
68
+ "concurrently": "^8.2.2",
69
+ "css-loader": "^7.1.1",
70
+ "eslint": "^8.57.0",
71
+ "eslint-config-airbnb-base": "^15.0.0",
72
+ "eslint-config-prettier": "^9.1.0",
73
+ "eslint-plugin-import": "^2.29.1",
74
+ "eslint-plugin-jest": "^27.9.0",
75
+ "eslint-plugin-prettier": "^5.1.3",
76
+ "eslint-plugin-security": "^2.1.1",
77
+ "eslint-plugin-storybook": "^0.8.0",
78
+ "eslint-webpack-plugin": "^4.1.0",
79
+ "file-loader": "^6.2.0",
80
+ "fs-extra": "^11.2.0",
81
+ "glob": "^10.3.12",
82
+ "graceful-fs": "^4.2.11",
83
+ "html-webpack-plugin": "^5.6.0",
84
+ "imagemin-webpack-plugin": "^2.4.2",
85
+ "jest": "^29.7.0",
86
+ "jest-environment-jsdom": "^29.7.0",
87
+ "js-yaml": "^4.1.0",
88
+ "js-yaml-loader": "^1.2.2",
89
+ "lint-staged": "^15.2.2",
90
+ "mini-css-extract-plugin": "^2.9.0",
91
+ "node-sass-glob-importer": "^5.3.3",
92
+ "normalize.css": "^8.0.1",
93
+ "open-cli": "^8.0.0",
94
+ "pa11y": "^7.0.0",
95
+ "postcss": "^8.4.38",
96
+ "postcss-loader": "^8.1.1",
97
+ "postcss-scss": "^4.0.9",
98
+ "ramda": "^0.29.1",
99
+ "react": "^18.2.0",
100
+ "react-dom": "^18.2.0",
101
+ "regenerator-runtime": "^0.14.1",
102
+ "sass": "^1.75.0",
103
+ "sass-loader": "^14.2.1",
104
+ "storybook": "^7.6.17",
105
+ "style-dictionary": "^3.9.2",
106
+ "stylelint": "^16.3.1",
107
+ "stylelint-config-standard-scss": "^13.1.0",
108
+ "stylelint-prettier": "^5.0.0",
109
+ "stylelint-selector-bem-pattern": "^4.0.0",
110
+ "stylelint-webpack-plugin": "^5.0.0",
111
+ "svg-sprite-loader": "^6.0.11",
112
+ "token-transformer": "^0.0.33",
113
+ "twig-drupal-filters": "^3.2.0",
114
+ "twig-loader": "github:fourkitchens/twig-loader",
115
+ "twig-testing-library": "^1.2.0",
116
+ "webpack": "^5.91.0",
117
+ "webpack-cli": "^5.1.4",
118
+ "webpack-merge": "^5.10.0",
119
+ "yaml": "^2.4.1"
120
+ },
121
+ "devDependencies": {
122
+ "@commitlint/cli": "^19.2.0",
123
+ "@commitlint/config-conventional": "^19.1.0",
124
+ "@semantic-release/changelog": "^6.0.2",
125
+ "@semantic-release/commit-analyzer": "^11.1.0",
126
+ "@semantic-release/git": "^10.0.1",
127
+ "@semantic-release/github": "^10.0.2",
128
+ "@semantic-release/release-notes-generator": "^12.1.0",
129
+ "husky": "^9.0.11",
130
+ "semantic-release": "^23.0.4"
131
+ },
132
+ "overrides": {
133
+ "graceful-fs": "^4.2.11"
134
+ },
135
+ "main": "commitlint.config.js"
136
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ tagFormat: '${version}',
3
+ branches: ['main'],
4
+ repositoryUrl: 'git@github.com:emulsify-ds/emulsify-core.git',
5
+ plugins: [
6
+ '@semantic-release/commit-analyzer',
7
+ '@semantic-release/release-notes-generator',
8
+ ['@semantic-release/npm', { npmPublish: false }],
9
+ '@semantic-release/github',
10
+ ],
11
+ };
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file a11y.js
4
+ * Contains a script that, when executed, will execute a11y linting tools
5
+ * against the storybook build.
6
+ */
7
+ const chalk = import("chalk").then(m => m.default);
8
+
9
+ const R = require('ramda');
10
+ const path = require('path');
11
+ const pa11y = require('pa11y');
12
+ const {
13
+ storybookBuildDir,
14
+ pa11y: pa11yConfig,
15
+ } = require('../config/a11y.config.js');
16
+ // project specific configuration.
17
+ const {
18
+ ignore,
19
+ components,
20
+ } = require('../../../config/emulsify-core/a11y.config.js');
21
+
22
+ const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
23
+ const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
24
+
25
+ const severityToColor = R.cond([
26
+ [R.equals('error'), R.always('red')],
27
+ [R.equals('warning'), R.always('yellow')],
28
+ [R.equals('notice'), R.always('blue')],
29
+ ]);
30
+
31
+ const issueIsValid = ({ code, runnerExtras: { description } }) =>
32
+ ignore.codes.includes(code) || ignore.descriptions.includes(description)
33
+ ? false
34
+ : true;
35
+
36
+ const logIssue = ({ type: severity, message, context, selector }) => {
37
+ console.log(`
38
+ severity: ${chalk[severityToColor(severity)](severity)}
39
+ message: ${message}
40
+ context: ${context}
41
+ selector: ${selector}
42
+ `);
43
+ };
44
+
45
+ const logReport = ({ issues, pageUrl }) => {
46
+ const validIssues = issues.filter(issueIsValid);
47
+ const hasIssues = validIssues.length > 0;
48
+
49
+ if (hasIssues) {
50
+ console.log(chalk.red(`Issues found in component: ${pageUrl}`));
51
+ validIssues.map(logIssue);
52
+ } else {
53
+ console.log(chalk.green(`No issues found in component: ${pageUrl}`));
54
+ }
55
+
56
+ return hasIssues;
57
+ };
58
+
59
+ const lintComponent = async (name) =>
60
+ pa11y(`${STORYBOOK_IFRAME}?id=${name}`, {
61
+ includeNotices: true,
62
+ includeWarnings: true,
63
+ runners: ['axe'],
64
+ ...pa11yConfig,
65
+ });
66
+
67
+ const lintReportAndExit = R.pipe(
68
+ R.map(lintComponent),
69
+ (p) => Promise.all(p),
70
+ R.andThen(
71
+ R.pipe(
72
+ R.map(logReport),
73
+ R.reject(R.equals(false)),
74
+ R.unless(R.isEmpty, () => process.exit(1)),
75
+ ),
76
+ ),
77
+ );
78
+
79
+ // Only perform linting/reporting when instructed.
80
+ /* istanbul ignore next */
81
+ if (R.pathEq(['argv', 2], '-r')(process)) {
82
+ lintReportAndExit(components);
83
+ }
84
+
85
+ module.exports = {
86
+ severityToColor,
87
+ issueIsValid,
88
+ logIssue,
89
+ logReport,
90
+ lintComponent,
91
+ lintReportAndExit,
92
+ };
@@ -0,0 +1,159 @@
1
+ import 'regenerator-runtime/runtime';
2
+
3
+ const mockExit = jest
4
+ .spyOn(global.process, 'exit')
5
+ .mockImplementation(() => {});
6
+ jest.mock('pa11y', () => jest.fn());
7
+ jest.spyOn(global.console, 'log').mockImplementation(() => {});
8
+ const pa11y = require('pa11y');
9
+ const path = require('path');
10
+ const {
11
+ severityToColor,
12
+ issueIsValid,
13
+ logIssue,
14
+ logReport,
15
+ lintComponent,
16
+ lintReportAndExit,
17
+ } = require('./a11y');
18
+ const {
19
+ ignore,
20
+ storybookBuildDir,
21
+ pa11y: pa11yConfig,
22
+ } = require('../config/a11y.config');
23
+
24
+ const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
25
+ const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
26
+
27
+ pa11y.mockResolvedValue('very official report');
28
+
29
+ describe('a11y', () => {
30
+ beforeEach(() => {
31
+ global.console.log.mockClear();
32
+ global.process.exit.mockClear();
33
+ });
34
+ it('can map axe issue severity to the correct chalk color', () => {
35
+ expect.assertions(3);
36
+ expect(severityToColor('error')).toBe('red');
37
+ expect(severityToColor('warning')).toBe('yellow');
38
+ expect(severityToColor('notice')).toBe('blue');
39
+ });
40
+
41
+ it('identifies invalid issues based on the code or the description', () => {
42
+ expect.assertions(3);
43
+ expect(
44
+ issueIsValid({
45
+ code: ignore.codes[0],
46
+ runnerExtras: {},
47
+ }),
48
+ ).toBe(false);
49
+ expect(
50
+ issueIsValid({
51
+ runnerExtras: {
52
+ description: ignore.descriptions[0],
53
+ },
54
+ }),
55
+ ).toBe(false);
56
+ expect(issueIsValid({ code: 'chicken', runnerExtras: {} })).toBe(true);
57
+ });
58
+
59
+ it('can use an axe issue to generate a single log message about the issue', () => {
60
+ expect.assertions(1);
61
+ logIssue({
62
+ type: 'error',
63
+ message: 'this chicken is not fried enough.',
64
+ context: 'https://example.com',
65
+ selector: 'kfc > popeyes > .chicken',
66
+ });
67
+ expect(global.console.log.mock.calls[0][0]).toMatchInlineSnapshot(`
68
+ "
69
+ severity: error
70
+ message: this chicken is not fried enough.
71
+ context: https://example.com
72
+ selector: kfc > popeyes > .chicken
73
+ "
74
+ `);
75
+ });
76
+
77
+ it('can log a whole axe report', () => {
78
+ const report = {
79
+ issues: [
80
+ {
81
+ type: 'error',
82
+ message: 'this pizza is too soggy',
83
+ context: 'https://example.com',
84
+ selector: 'pizza > .hut',
85
+ runnerExtras: {},
86
+ },
87
+ {
88
+ type: 'error',
89
+ message: 'this pasta is undercooked',
90
+ context: 'https://example.com',
91
+ selector: 'olive > .garden',
92
+ runnerExtras: {},
93
+ },
94
+ ],
95
+ pageUrl: 'https://example/component.html',
96
+ };
97
+ expect(logReport(report)).toBe(true);
98
+ expect(global.console.log.mock.calls).toMatchInlineSnapshot(`
99
+ Array [
100
+ Array [
101
+ "Issues found in component: https://example/component.html",
102
+ ],
103
+ Array [
104
+ "
105
+ severity: error
106
+ message: this pizza is too soggy
107
+ context: https://example.com
108
+ selector: pizza > .hut
109
+ ",
110
+ ],
111
+ Array [
112
+ "
113
+ severity: error
114
+ message: this pasta is undercooked
115
+ context: https://example.com
116
+ selector: olive > .garden
117
+ ",
118
+ ],
119
+ ]
120
+ `);
121
+ });
122
+
123
+ it('logs about a component having no issue if a report comes back empty', () => {
124
+ expect(logReport({ issues: [], pageUrl: 'papa-johns' })).toBe(false);
125
+ expect(global.console.log.mock.calls[0][0]).toMatchInlineSnapshot(
126
+ `"No issues found in component: papa-johns"`,
127
+ );
128
+ });
129
+
130
+ it('can call pa11y with the full path to a component', async () => {
131
+ expect.assertions(2);
132
+ await expect(lintComponent('chicken-strips')).resolves.toBe(
133
+ 'very official report',
134
+ );
135
+ expect(pa11y).toHaveBeenCalledWith(
136
+ `${STORYBOOK_IFRAME}?id=chicken-strips`,
137
+ pa11yConfig,
138
+ );
139
+ });
140
+
141
+ it('runs linter, reports on issues, and exits with code "1" if valid issues are found', async () => {
142
+ expect.assertions(1);
143
+ pa11y.mockResolvedValueOnce({
144
+ issues: [
145
+ {
146
+ type: 'error',
147
+ message: 'these 7 layer supreme burritos do not taste that good',
148
+ context: 'https://example.com',
149
+ selector: 'taco > bell > .burrito',
150
+ runnerExtras: {},
151
+ },
152
+ ],
153
+ pageUrl: '/path/to/taco-bell',
154
+ });
155
+
156
+ await lintReportAndExit(['taco-bell']);
157
+ expect(global.process.exit).toHaveBeenCalledWith(1);
158
+ });
159
+ });
@@ -0,0 +1,17 @@
1
+ import { resolve } from 'path';
2
+ import { readFileSync } from 'fs';
3
+ import { parse } from 'yaml';
4
+ import R from 'ramda';
5
+
6
+ /**
7
+ * Small utility function that loads a yaml file and parses it synchronously.
8
+ * This is intended to make composition cleaner.
9
+ *
10
+ * @param {string} relativePath - relative path to a yaml file that will be loaded and parsed.
11
+ *
12
+ * @returns {string} JavaScript object that results from the yaml parsing of the specified file.
13
+ */
14
+ export default function loadYaml(relativePath) {
15
+ const fullPath = resolve(__dirname, relativePath);
16
+ return parse(readFileSync(fullPath, 'utf8'));
17
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'fs';
2
+ import yaml from 'yaml';
3
+ import loadYaml from './loadYaml';
4
+
5
+ jest
6
+ .spyOn(fs, 'readFileSync')
7
+ .mockImplementation(() => 'yaml spaghetti and meatballs');
8
+
9
+ jest.spyOn(yaml, 'parse').mockImplementation(() => ({
10
+ the: 'yaml spaghetti and meatballs',
11
+ }));
12
+
13
+ describe('loadYaml', () => {
14
+ beforeEach(() => {
15
+ fs.readFileSync.mockClear();
16
+ yaml.parse.mockClear();
17
+ });
18
+
19
+ it('can load a yaml file, parse it, and return it', () => {
20
+ expect.assertions(3);
21
+ expect(loadYaml('./big-phat-burger.yml')).toEqual({
22
+ the: 'yaml spaghetti and meatballs',
23
+ });
24
+ expect(fs.readFileSync).toHaveBeenCalledWith(
25
+ `${__dirname}/big-phat-burger.yml`,
26
+ 'utf8',
27
+ );
28
+ expect(yaml.parse).toHaveBeenCalledWith('yaml spaghetti and meatballs');
29
+ });
30
+ });