@emulsify/core 3.5.0 → 4.0.1

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 (110) hide show
  1. package/.cli/init.js +40 -31
  2. package/.storybook/_drupal.js +129 -8
  3. package/.storybook/css-components.js +13 -0
  4. package/.storybook/css-dist.js +5 -0
  5. package/.storybook/emulsifyTheme.js +9 -6
  6. package/.storybook/main.js +397 -106
  7. package/.storybook/manager.js +9 -16
  8. package/.storybook/preview.js +88 -110
  9. package/.storybook/utils.js +69 -74
  10. package/README.md +110 -59
  11. package/config/.stylelintrc.json +2 -6
  12. package/config/a11y.config.js +9 -5
  13. package/config/babel.config.js +6 -11
  14. package/config/eslint.config.js +31 -3
  15. package/config/postcss.config.js +5 -0
  16. package/config/vite/entries.js +227 -0
  17. package/config/vite/environment.js +39 -0
  18. package/config/vite/platforms.js +70 -0
  19. package/config/vite/plugins/copy-src-assets.js +76 -0
  20. package/config/vite/plugins/copy-twig-files.js +84 -0
  21. package/config/vite/plugins/css-asset-relativizer.js +40 -0
  22. package/config/vite/plugins/index.js +105 -0
  23. package/config/vite/plugins/mirror-components.js +358 -0
  24. package/config/vite/plugins/require-context.js +311 -0
  25. package/config/vite/plugins/source-file-index.js +184 -0
  26. package/config/vite/plugins/svg-sprite.js +117 -0
  27. package/config/vite/plugins/twig-extension-installers.js +36 -0
  28. package/config/vite/plugins/twig-module.js +1251 -0
  29. package/config/vite/plugins/virtual-twig-asset-sources.js +404 -0
  30. package/config/vite/plugins/virtual-twig-globs.js +136 -0
  31. package/config/vite/plugins/vituum-patch.js +167 -0
  32. package/config/vite/plugins/yaml-module.js +133 -0
  33. package/config/vite/plugins.js +12 -0
  34. package/config/vite/project-config.js +192 -0
  35. package/config/vite/project-extensions.js +177 -0
  36. package/config/vite/project-structure.js +447 -0
  37. package/config/vite/twig-extensions.js +109 -0
  38. package/config/vite/utils/fs-safe.js +66 -0
  39. package/config/vite/utils/paths.js +40 -0
  40. package/config/vite/utils/react-singleton.js +85 -0
  41. package/config/vite/utils/unique.js +36 -0
  42. package/config/vite/vite.config.js +161 -0
  43. package/package.json +164 -75
  44. package/scripts/a11y.js +70 -16
  45. package/scripts/audit-twig-stories.js +378 -0
  46. package/scripts/audit.js +1602 -0
  47. package/scripts/check-node-version.js +18 -0
  48. package/scripts/loadYaml.js +5 -1
  49. package/src/extensions/index.js +8 -0
  50. package/src/extensions/react/index.js +12 -0
  51. package/src/extensions/react/register.js +45 -0
  52. package/src/extensions/shared/attributes.js +308 -0
  53. package/src/extensions/shared/html.js +41 -0
  54. package/src/extensions/shared/lists.js +38 -0
  55. package/src/extensions/shared/object.js +22 -0
  56. package/src/extensions/twig/function-map.js +20 -0
  57. package/src/extensions/twig/functions/add-attributes.js +39 -0
  58. package/src/extensions/twig/functions/bem.js +166 -0
  59. package/src/extensions/twig/index.js +13 -0
  60. package/src/extensions/twig/register.js +95 -0
  61. package/src/extensions/twig/tag-map.js +16 -0
  62. package/src/extensions/twig/tags/switch.js +266 -0
  63. package/src/storybook/index.js +14 -0
  64. package/src/storybook/main-config.js +132 -0
  65. package/src/storybook/platform-behaviors.js +60 -0
  66. package/src/storybook/preview-parameters.js +81 -0
  67. package/src/storybook/render-twig.js +295 -0
  68. package/src/storybook/twig/drupal-filters.js +7 -0
  69. package/src/storybook/twig/include-function.js +109 -0
  70. package/src/storybook/twig/include.js +28 -0
  71. package/src/storybook/twig/reference-paths.js +294 -0
  72. package/src/storybook/twig/resolver.js +318 -0
  73. package/src/storybook/twig/setup.js +39 -0
  74. package/src/storybook/twig/source-events.js +5 -0
  75. package/src/storybook/twig/source-extensions.js +24 -0
  76. package/src/storybook/twig/source-function.js +239 -0
  77. package/src/storybook/twig/source.js +39 -0
  78. package/.all-contributorsrc +0 -45
  79. package/.editorconfig +0 -5
  80. package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +0 -18
  81. package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +0 -11
  82. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  83. package/.github/dependabot.yml +0 -6
  84. package/.github/workflows/addtoprojects.yml +0 -21
  85. package/.github/workflows/contributors.yml +0 -37
  86. package/.github/workflows/lint.yml +0 -22
  87. package/.github/workflows/semantic-release.yml +0 -24
  88. package/.husky/commit-msg +0 -2
  89. package/.husky/pre-commit +0 -2
  90. package/.nvmrc +0 -1
  91. package/.prettierignore +0 -4
  92. package/.storybook/polyfills/twig-include.js +0 -40
  93. package/.storybook/polyfills/twig-resolver.js +0 -70
  94. package/.storybook/polyfills/twig-source.js +0 -65
  95. package/.storybook/webpack.config.js +0 -269
  96. package/CODE_OF_CONDUCT.md +0 -56
  97. package/commitlint.config.js +0 -5
  98. package/config/jest.config.js +0 -19
  99. package/config/webpack/app.js +0 -1
  100. package/config/webpack/loaders.js +0 -167
  101. package/config/webpack/optimizers.js +0 -26
  102. package/config/webpack/plugins.js +0 -283
  103. package/config/webpack/resolves.js +0 -157
  104. package/config/webpack/sdc-loader.js +0 -16
  105. package/config/webpack/webpack.common.js +0 -272
  106. package/config/webpack/webpack.dev.js +0 -41
  107. package/config/webpack/webpack.prod.js +0 -6
  108. package/release.config.cjs +0 -30
  109. package/scripts/a11y.test.js +0 -172
  110. package/scripts/loadYaml.test.js +0 -30
package/.cli/init.js CHANGED
@@ -1,26 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const yaml = require('js-yaml');
3
+ /**
4
+ * @file Initializes a generated Emulsify project from project.emulsify.json.
5
+ */
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import yaml from 'js-yaml';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
13
 
7
14
  /**
8
- * Returns a boolean indicating whether or not the given object is a literal object.
15
+ * Determine whether a value is a plain object.
9
16
  *
10
- * @param {any} obj object who's type will be checked.
11
- * @returns {boolean} boolean indicating whether or not the given obj is a literal object.
17
+ * @param {*} obj - Value to inspect.
18
+ * @returns {boolean} TRUE when the value is a plain object.
12
19
  */
13
20
  const isObjectLiteral = (obj) =>
14
21
  obj != null && obj.constructor.name === 'Object';
15
22
 
16
23
  /**
17
- * Attempts to require the project.emulsify.json file.
24
+ * Load project.emulsify.json from the generated project config directory.
18
25
  *
19
- * @returns parsed project.emulsify.json file.
26
+ * @returns {Object} Parsed project.emulsify.json file.
27
+ * @throws {Error} When the config cannot be loaded.
20
28
  */
21
29
  const getEmulsifyConfig = () => {
30
+ const configPath = path.join(__dirname, '../config/project.emulsify.json');
31
+
22
32
  try {
23
- return require('../config/project.emulsify.json');
33
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
24
34
  } catch (e) {
25
35
  throw new Error(
26
36
  `Unable to load an Emulsify project config file (project.emulsify.json): ${String(
@@ -31,9 +41,11 @@ const getEmulsifyConfig = () => {
31
41
  };
32
42
 
33
43
  /**
34
- * Throws if the given emulsify config file is invalid.
44
+ * Validate the minimal project configuration required for initialization.
35
45
  *
36
- * @param {*} config emulsify project config, as loaded from a project.emulsify.json file.
46
+ * @param {*} config - Emulsify project config loaded from project.emulsify.json.
47
+ * @returns {void}
48
+ * @throws {Error} When required config values are missing or invalid.
37
49
  */
38
50
  const validateEmulsifyConfig = (config) => {
39
51
  const prefix = 'Invalid project.emulsify.json config file';
@@ -68,11 +80,10 @@ const validateEmulsifyConfig = (config) => {
68
80
  };
69
81
 
70
82
  /**
71
- * Takes an array of objects describing the origin and destination of a given file,
72
- * then moves each specified file according to it's to/from properties.
83
+ * Move generated starter files to their project-specific names.
73
84
  *
74
- * @param {Array<{ to: string, from: string }>} files array of objects depicting the origin and destination of a given file.
75
- * @returns void.
85
+ * @param {Array<{ to: string, from: string }>} files - Files to move.
86
+ * @returns {Array<void>} Rename results.
76
87
  */
77
88
  const renameFiles = (files) =>
78
89
  files.map(({ from, to }) =>
@@ -80,24 +91,23 @@ const renameFiles = (files) =>
80
91
  );
81
92
 
82
93
  /**
83
- * Takes a machineName, and returns a fn that, when called with a str,
84
- * replaces all instances of `emulsify` with the given machineName.
94
+ * Create a replacer that swaps the starter machine name for the project name.
85
95
  *
86
- * @param {string} machineName string that should replace emulsify.
87
- * @returns {function} fn that when called with a str, replaces all instances of `emulsify` with the given machineName.
96
+ * @param {string} machineName - Machine name that should replace `emulsify`.
97
+ * @returns {Function} String replacer.
88
98
  */
89
99
  const strReplaceEmulsify = (machineName) => (str) =>
90
100
  str.replace(/emulsify/g, machineName);
91
101
 
92
102
  /**
93
- * Loads a yml file at filePath, applies the functor to the contents of the file, and writes it.
103
+ * Load a YAML file, transform its parsed contents, and write it back.
94
104
  *
95
- * @param {string} filePath path to the file that should be loaded, modified, and re-saved.
96
- * @param {fn} functor fn that should return the new contents of the file, to be saved.
97
- * @returns void.
105
+ * @param {string} filePath - File to load, modify, and save.
106
+ * @param {Function} functor - Function that returns the replacement YAML data.
107
+ * @returns {void}
98
108
  */
99
109
  const applyToYmlFile = (filePath, functor) => {
100
- if (!filePath || typeof filePath !== `string`) {
110
+ if (!filePath || typeof filePath !== 'string') {
101
111
  throw new Error(
102
112
  `Cannot modify a file without knowing how to access it: ${filePath}`,
103
113
  );
@@ -111,18 +121,17 @@ const applyToYmlFile = (filePath, functor) => {
111
121
  };
112
122
 
113
123
  const main = () => {
114
- // Load up config file, throw if none exists.
124
+ // Load the project config before mutating any generated files.
115
125
  const config = getEmulsifyConfig();
116
126
 
117
- // Validate config file, throw if it is missing
118
- //properties or is otherwise malformed.
127
+ // Fail fast when required project metadata is missing or malformed.
119
128
  validateEmulsifyConfig(config);
120
129
 
121
130
  const {
122
- project: { machineName, name },
131
+ project: { machineName },
123
132
  } = config;
124
133
 
125
- // Move all files to their correct location.
134
+ // Rename starter files from the generic prefix to the project machine name.
126
135
  renameFiles([
127
136
  {
128
137
  from: '../emulsify.info.yml',
@@ -142,7 +151,7 @@ const main = () => {
142
151
  },
143
152
  ]);
144
153
 
145
- // Update info.yml file.
154
+ // Update info.yml values that Drupal reads from the generated theme.
146
155
  applyToYmlFile(
147
156
  path.join(__dirname, `../${machineName}.info.yml`),
148
157
  (info) => ({
@@ -152,7 +161,7 @@ const main = () => {
152
161
  }),
153
162
  );
154
163
 
155
- // Update breakpoint.yml file.
164
+ // Update breakpoint keys to match the renamed theme machine name.
156
165
  applyToYmlFile(
157
166
  path.join(__dirname, `../${machineName}.breakpoints.yml`),
158
167
  (breakpoints) => {
@@ -1,10 +1,96 @@
1
- // Simple Drupal.behaviors usage for Storybook
1
+ /**
2
+ * @file Drupal browser compatibility layer for Storybook.
3
+ */
4
+
5
+ const emulsifyEnv =
6
+ (typeof __EMULSIFY_ENV__ !== 'undefined' && __EMULSIFY_ENV__) || {};
7
+ const projectMachineName =
8
+ typeof emulsifyEnv.machineName === 'string' ? emulsifyEnv.machineName : '';
9
+
10
+ /**
11
+ * Storybook-safe defaults for the Drupal settings object.
12
+ *
13
+ * These values cover the common browser properties Drupal-authored JavaScript
14
+ * reads while keeping project-specific module settings in project overrides.
15
+ *
16
+ * @type {object}
17
+ */
18
+ const defaultDrupalSettings = {
19
+ path: {
20
+ baseUrl: '/',
21
+ currentLanguage: 'en',
22
+ isFront: false,
23
+ langcode: 'en',
24
+ pathPrefix: '',
25
+ currentPath: '/',
26
+ currentPathIsAdmin: false,
27
+ },
28
+ user: {
29
+ uid: 0,
30
+ permissionsHash: '',
31
+ },
32
+ ajaxPageState: {
33
+ theme: projectMachineName,
34
+ theme_token: '',
35
+ },
36
+ ajaxTrustedUrl: {},
37
+ pluralDelimiter: '\u0003',
38
+ };
39
+
40
+ /**
41
+ * Determine whether a value can be recursively merged as settings.
42
+ *
43
+ * @param {*} value - Candidate value.
44
+ * @returns {boolean} TRUE when the value is a plain object.
45
+ */
46
+ function isPlainObject(value) {
47
+ return (
48
+ Boolean(value) &&
49
+ Object.prototype.toString.call(value) === '[object Object]'
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Merge default settings with project-provided Drupal settings.
55
+ *
56
+ * Existing project settings win so projects can provide module-specific values
57
+ * or override neutral defaults from `config/emulsify-core/storybook/preview.js`.
58
+ *
59
+ * @param {object} defaults - Default Drupal settings.
60
+ * @param {object} overrides - Project-provided Drupal settings.
61
+ * @returns {object} Merged settings object.
62
+ */
63
+ function mergeDrupalSettings(defaults, overrides) {
64
+ const merged = { ...defaults };
65
+
66
+ for (const [key, value] of Object.entries(overrides || {})) {
67
+ // Drupal settings keys are project/module-defined by design.
68
+ // eslint-disable-next-line security/detect-object-injection
69
+ const defaultValue = merged[key];
70
+ const nextValue =
71
+ isPlainObject(defaultValue) && isPlainObject(value)
72
+ ? mergeDrupalSettings(defaultValue, value)
73
+ : value;
74
+
75
+ // Drupal settings keys are project/module-defined by design.
76
+ // eslint-disable-next-line security/detect-object-injection
77
+ merged[key] = nextValue;
78
+ }
79
+
80
+ return merged;
81
+ }
2
82
 
3
83
  /**
4
- * Global Drupal namespace stub for Storybook environment.
84
+ * Create the global Drupal namespace stub for the Storybook environment.
85
+ *
5
86
  * @namespace Drupal
6
87
  */
7
- window.Drupal = { behaviors: {} };
88
+ window.Drupal = window.Drupal || {};
89
+ window.Drupal.behaviors = window.Drupal.behaviors || {};
90
+ window.drupalSettings = mergeDrupalSettings(
91
+ defaultDrupalSettings,
92
+ isPlainObject(window.drupalSettings) ? window.drupalSettings : {},
93
+ );
8
94
 
9
95
  /**
10
96
  * Immediately-Invoked Function Expression to scope Drupal behavior attachment logic.
@@ -12,6 +98,38 @@ window.Drupal = { behaviors: {} };
12
98
  * @param {Object} drupalSettings - Global Drupal settings object stub.
13
99
  */
14
100
  (function (Drupal, drupalSettings) {
101
+ /**
102
+ * Replaces Drupal-style string placeholders without translating the string.
103
+ *
104
+ * @param {string} str - String containing placeholders such as `@name`.
105
+ * @param {Object.<string, string|number>} [args={}] - Placeholder values.
106
+ * @returns {string} Formatted string.
107
+ */
108
+ Drupal.formatString =
109
+ Drupal.formatString ||
110
+ function (str, args = {}) {
111
+ let formatted = String(str);
112
+
113
+ for (const [placeholder, replacement] of Object.entries(args || {})) {
114
+ formatted = formatted.split(placeholder).join(String(replacement));
115
+ }
116
+
117
+ return formatted;
118
+ };
119
+
120
+ /**
121
+ * Minimal translation shim for Drupal-authored JavaScript in Storybook.
122
+ *
123
+ * @param {string} str - Source string.
124
+ * @param {Object.<string, string|number>} [args={}] - Placeholder values.
125
+ * @returns {string} Formatted source string.
126
+ */
127
+ Drupal.t =
128
+ Drupal.t ||
129
+ function (str, args = {}) {
130
+ return Drupal.formatString(str, args);
131
+ };
132
+
15
133
  /**
16
134
  * Throws an error asynchronously to avoid interrupting execution flow.
17
135
  * @param {Error} error - The error object to throw.
@@ -32,11 +150,14 @@ window.Drupal = { behaviors: {} };
32
150
  Drupal.attachBehaviors = function (context, settings) {
33
151
  context = context || document;
34
152
  settings = settings || drupalSettings;
35
- /** @type {Array<{attach?: Function}>} */
36
- const behaviors = Object.values(Drupal.behaviors);
153
+ /** @type {Object.<string, {attach: Function}>} */
154
+ const behaviors = Drupal.behaviors;
37
155
 
38
- // Iterate through each behavior and invoke its attach method if defined.
39
- behaviors.forEach(function (behavior) {
156
+ // Attach each registered behavior while isolating individual failures.
157
+ Object.keys(behaviors).forEach(function (behaviorName) {
158
+ // Drupal behavior names are project/module-defined by design.
159
+ // eslint-disable-next-line security/detect-object-injection
160
+ const behavior = behaviors[behaviorName];
40
161
  if (typeof behavior.attach === 'function') {
41
162
  try {
42
163
  behavior.attach(context, settings);
@@ -46,4 +167,4 @@ window.Drupal = { behaviors: {} };
46
167
  }
47
168
  });
48
169
  };
49
- })(Drupal, window.drupalSettings);
170
+ })(window.Drupal, window.drupalSettings);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file Storybook mirrored component CSS side-effect loader.
3
+ *
4
+ * Drupal projects load component CSS from the mirrored root components tree,
5
+ * but shared/global Storybook CSS still lives under dist. Exclude
6
+ * dist/components so the mirrored component CSS is not loaded twice.
7
+ */
8
+
9
+ import.meta.glob('../../../../components/**/*.css', { eager: true });
10
+ import.meta.glob(
11
+ ['../../../../dist/**/*.css', '!../../../../dist/components/**/*.css'],
12
+ { eager: true },
13
+ );
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file Storybook compiled dist CSS side-effect loader.
3
+ */
4
+
5
+ import.meta.glob('../../../../dist/**/*.css', { eager: true });
@@ -1,16 +1,21 @@
1
- // Documentation on theming Storybook: https://storybook.js.org/docs/configurations/theming/
1
+ /**
2
+ * @file Default Emulsify theme for the Storybook manager UI.
3
+ *
4
+ * @see https://storybook.js.org/docs/configurations/theming/
5
+ */
6
+
2
7
  import { create } from 'storybook/theming';
3
8
 
4
9
  export default create({
5
10
  base: 'dark',
6
11
 
7
- // UI
12
+ // Storybook application chrome colors.
8
13
  appBg: '#00405B',
9
14
  appContentBg: '#00202E',
10
15
  appBorderColor: '#00405B',
11
16
  appBorderRadius: 4,
12
17
 
13
- // Typography
18
+ // Typography is intentionally aligned with the design system brand.
14
19
  fontBase: '"Mona Sans", sans-serif',
15
20
  fontCode: 'monospace',
16
21
 
@@ -29,9 +34,7 @@ export default create({
29
34
  inputBorder: '#00405B',
30
35
  inputTextColor: 'white',
31
36
  inputBorderRadius: 4,
32
- // Branding
37
+ // Branding links the manager back to the public Emulsify site.
33
38
  brandTitle: 'Emulsify',
34
39
  brandUrl: 'https://emulsify.info',
35
- brandImage:
36
- 'https://raw.githubusercontent.com/fourkitchens/emulsify-core/main/assets/images/emulsify-logo-sb.svg',
37
40
  });