@emulsify/core 4.0.0 → 4.0.2

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.
package/.cli/init.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
- import yaml from 'js-yaml';
10
+ import { dump as dumpYaml, load as loadYaml } from 'js-yaml';
11
11
 
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
 
@@ -116,8 +116,8 @@ const applyToYmlFile = (filePath, functor) => {
116
116
  return;
117
117
  }
118
118
 
119
- const file = yaml.load(fs.readFileSync(filePath, 'utf8'));
120
- fs.writeFileSync(filePath, yaml.dump(functor(file)));
119
+ const file = loadYaml(fs.readFileSync(filePath, 'utf8'));
120
+ fs.writeFileSync(filePath, dumpYaml(functor(file)));
121
121
  };
122
122
 
123
123
  const main = () => {
@@ -65,7 +65,6 @@ function mergeDrupalSettings(defaults, overrides) {
65
65
 
66
66
  for (const [key, value] of Object.entries(overrides || {})) {
67
67
  // Drupal settings keys are project/module-defined by design.
68
- // eslint-disable-next-line security/detect-object-injection
69
68
  const defaultValue = merged[key];
70
69
  const nextValue =
71
70
  isPlainObject(defaultValue) && isPlainObject(value)
@@ -73,7 +72,6 @@ function mergeDrupalSettings(defaults, overrides) {
73
72
  : value;
74
73
 
75
74
  // Drupal settings keys are project/module-defined by design.
76
- // eslint-disable-next-line security/detect-object-injection
77
75
  merged[key] = nextValue;
78
76
  }
79
77
 
@@ -156,7 +154,6 @@ window.drupalSettings = mergeDrupalSettings(
156
154
  // Attach each registered behavior while isolating individual failures.
157
155
  Object.keys(behaviors).forEach(function (behaviorName) {
158
156
  // Drupal behavior names are project/module-defined by design.
159
- // eslint-disable-next-line security/detect-object-injection
160
157
  const behavior = behaviors[behaviorName];
161
158
  if (typeof behavior.attach === 'function') {
162
159
  try {
@@ -70,6 +70,27 @@ const _filename = fileURLToPath(import.meta.url);
70
70
  */
71
71
  const _dirname = path.dirname(_filename);
72
72
 
73
+ /**
74
+ * The consuming project root for Storybook static mounts.
75
+ *
76
+ * Storybook loads this package config from different physical locations
77
+ * depending on whether Core is linked locally or installed in node_modules, so
78
+ * static paths must be rooted at the process cwd rather than this file.
79
+ *
80
+ * @type {string}
81
+ */
82
+ const projectRoot = process.cwd();
83
+
84
+ /**
85
+ * Vite-generated Storybook chunks should not share `/assets` with project
86
+ * static files. Storybook copies staticDirs while the preview build runs, so
87
+ * keeping generated chunks in a separate folder avoids concurrent writers in
88
+ * `.out/assets`.
89
+ *
90
+ * @type {string}
91
+ */
92
+ const storybookViteAssetsDir = 'storybook-assets';
93
+
73
94
  /**
74
95
  * Reads an optional HTML fragment relative to this config file.
75
96
  *
@@ -247,10 +268,17 @@ const baseConfig = {
247
268
  staticDirs: [
248
269
  ...existingStaticDirs([
249
270
  {
250
- from: path.resolve(process.cwd(), 'assets'),
271
+ from: path.resolve(projectRoot, 'assets'),
272
+ to: '/assets',
273
+ },
274
+ {
275
+ from: path.resolve(projectRoot, 'dist/assets'),
251
276
  to: '/assets',
252
277
  },
253
- path.resolve(process.cwd(), 'dist'),
278
+ {
279
+ from: path.resolve(projectRoot, 'dist'),
280
+ to: '/dist',
281
+ },
254
282
  ]),
255
283
  ],
256
284
 
@@ -452,6 +480,7 @@ const baseConfig = {
452
480
  const { mergeConfig } = await import('vite');
453
481
  /** @type {StorybookEnvironment} */
454
482
  const env = resolvedStorybookEnv;
483
+ const storybookBuildConfig = config?.build || {};
455
484
 
456
485
  // Keep using the `serve` branch of the shared Vite config here. Storybook
457
486
  // has historically consumed that branch, while `mode` still reflects
@@ -560,6 +589,14 @@ const baseConfig = {
560
589
 
561
590
  return {
562
591
  ...mergedConfig,
592
+ build: {
593
+ ...(mergedConfig.build || {}),
594
+ ...(storybookBuildConfig.outDir
595
+ ? { outDir: storybookBuildConfig.outDir }
596
+ : {}),
597
+ assetsDir: storybookViteAssetsDir,
598
+ emptyOutDir: false,
599
+ },
563
600
  resolve: mergeReactSingletonResolve(mergedConfig),
564
601
  optimizeDeps: {
565
602
  ...(mergedConfig.optimizeDeps || {}),
@@ -41,9 +41,15 @@ export default [
41
41
  ignores: ['**/*.min.js', '**/node_modules/**/*'],
42
42
 
43
43
  rules: {
44
- // Keep historical project conventions while warning on risky patterns.
44
+ // Keep historical project conventions while tuning noisy security
45
+ // heuristics that flag intentional file-generation and source-scanning
46
+ // patterns throughout this tooling package.
45
47
  strict: 0,
46
48
  'consistent-return': 'off',
49
+ 'security/detect-non-literal-fs-filename': 'off',
50
+ 'security/detect-non-literal-regexp': 'off',
51
+ 'security/detect-object-injection': 'off',
52
+ 'security/detect-unsafe-regex': 'off',
47
53
  'no-underscore-dangle': 'off',
48
54
  'max-nested-callbacks': ['warn', 3],
49
55
  'import/extensions': 'off',
@@ -57,7 +57,7 @@ export const sanitizePath = (s) => s.replace(/[^a-zA-Z0-9/_-]/g, '');
57
57
  function safeSetKey(map, key, value) {
58
58
  const forbidden = ['__proto__', 'prototype', 'constructor'];
59
59
  if (!key || forbidden.some((bad) => key.includes(bad))) return;
60
- map[key] = value; // eslint-disable-line security/detect-object-injection
60
+ map[key] = value;
61
61
  }
62
62
 
63
63
  /** Return an absolute path from a source index entry or string. */
@@ -68,7 +68,6 @@ const pruneEmptyDirsUpTo = (startDir, stopAtDir) => {
68
68
 
69
69
  const isEmpty = (dir) => {
70
70
  try {
71
- // eslint-disable-next-line security/detect-non-literal-fs-filename
72
71
  return readdirSync(dir).length === 0;
73
72
  } catch {
74
73
  return false;
@@ -79,7 +78,6 @@ const pruneEmptyDirsUpTo = (startDir, stopAtDir) => {
79
78
  if (!isEmpty(cursor)) break;
80
79
 
81
80
  try {
82
- // eslint-disable-next-line security/detect-non-literal-fs-filename
83
81
  rmdirSync(cursor);
84
82
  } catch {
85
83
  // Stop at the first directory that cannot be removed.
@@ -103,9 +101,7 @@ const pruneEmptyDirsUpTo = (startDir, stopAtDir) => {
103
101
  */
104
102
  export const filesHaveSameBytes = (sourceFile, destinationFile) => {
105
103
  try {
106
- // eslint-disable-next-line security/detect-non-literal-fs-filename
107
104
  const sourceStats = statSync(sourceFile);
108
- // eslint-disable-next-line security/detect-non-literal-fs-filename
109
105
  const destinationStats = statSync(destinationFile);
110
106
  if (!destinationStats.isFile()) return false;
111
107
  if (sourceStats.size !== destinationStats.size) return false;
@@ -117,10 +113,8 @@ export const filesHaveSameBytes = (sourceFile, destinationFile) => {
117
113
 
118
114
  const sourceBuffer = Buffer.allocUnsafe(FILE_COMPARE_CHUNK_SIZE);
119
115
  const destinationBuffer = Buffer.allocUnsafe(FILE_COMPARE_CHUNK_SIZE);
120
- // eslint-disable-next-line security/detect-non-literal-fs-filename
121
116
  const sourceHandle = openSync(sourceFile, 'r');
122
117
  try {
123
- // eslint-disable-next-line security/detect-non-literal-fs-filename
124
118
  const destinationHandle = openSync(destinationFile, 'r');
125
119
  try {
126
120
  let position = 0;
@@ -175,7 +169,6 @@ export const filesHaveSameBytes = (sourceFile, destinationFile) => {
175
169
  */
176
170
  const isSymlink = (filePath) => {
177
171
  try {
178
- // eslint-disable-next-line security/detect-non-literal-fs-filename
179
172
  return lstatSync(filePath).isSymbolicLink();
180
173
  } catch {
181
174
  return false;
@@ -189,7 +182,6 @@ const isSymlink = (filePath) => {
189
182
  */
190
183
  const removeSourceFile = (sourceFile) => {
191
184
  try {
192
- // eslint-disable-next-line security/detect-non-literal-fs-filename
193
185
  unlinkSync(sourceFile);
194
186
  } catch (error) {
195
187
  if (error?.code !== 'ENOENT') throw error;
@@ -221,12 +213,10 @@ const copyFileIntoPlace = (sourceFile, destinationFile) => {
221
213
 
222
214
  try {
223
215
  copyFileSync(sourceFile, tempDestination);
224
- // eslint-disable-next-line security/detect-non-literal-fs-filename
225
216
  renameSync(tempDestination, destinationFile);
226
217
  removeSourceFile(sourceFile);
227
218
  } catch (error) {
228
219
  try {
229
- // eslint-disable-next-line security/detect-non-literal-fs-filename
230
220
  unlinkSync(tempDestination);
231
221
  } catch {
232
222
  /* noop */
@@ -255,7 +245,6 @@ const moveFileIntoPlace = (sourceFile, destinationFile) => {
255
245
  }
256
246
 
257
247
  try {
258
- // eslint-disable-next-line security/detect-non-literal-fs-filename
259
248
  renameSync(sourceFile, destinationFile);
260
249
  } catch (error) {
261
250
  if (error?.code !== 'EXDEV') throw error;
@@ -282,7 +271,6 @@ const readMirrorState = (markerFile) => {
282
271
  */
283
272
  const writeMirrorState = (markerFile, state) => {
284
273
  mkdirSync(dirname(markerFile), { recursive: true });
285
- // eslint-disable-next-line security/detect-non-literal-fs-filename
286
274
  writeFileSync(markerFile, `${JSON.stringify(state, null, 2)}\n`);
287
275
  };
288
276
 
@@ -43,7 +43,6 @@ export function walkFiles(
43
43
 
44
44
  let entryNames = [];
45
45
  try {
46
- // eslint-disable-next-line security/detect-non-literal-fs-filename
47
46
  entryNames = readdirSync(currentDir, { withFileTypes: true }).sort(
48
47
  (a, b) => a.name.localeCompare(b.name),
49
48
  );
@@ -82,7 +82,6 @@ export function svgSpriteFilePlugin({ include, symbolId = '[name]' }) {
82
82
  .map((abs) => {
83
83
  let content = '';
84
84
  try {
85
- // eslint-disable-next-line security/detect-non-literal-fs-filename
86
85
  content = readFileSync(abs, 'utf8');
87
86
  } catch {
88
87
  return '';
@@ -332,7 +332,6 @@ const buildTemplateFileCandidates = (baseDir, templatePath) => {
332
332
  const findExistingTemplateFile = (paths) =>
333
333
  paths.filter(Boolean).find((filePath) => {
334
334
  try {
335
- // eslint-disable-next-line security/detect-non-literal-fs-filename
336
335
  return fs.statSync(filePath).isFile();
337
336
  } catch {
338
337
  return false;
@@ -503,7 +502,6 @@ const parseTwigNamespaceReference = (templatePath, namespaces = {}) => {
503
502
  return {
504
503
  namespace: slashNamespace,
505
504
  // Namespace names come from the normalized Twig namespace map.
506
- // eslint-disable-next-line security/detect-object-injection
507
505
  root: namespaces[slashNamespace],
508
506
  path: templatePath.slice(slashNamespace.length + 1),
509
507
  };
@@ -523,7 +521,6 @@ const componentGroupRoots = (componentRoot) => {
523
521
 
524
522
  try {
525
523
  // Component group roots come from a configured project directory.
526
- // eslint-disable-next-line security/detect-non-literal-fs-filename
527
524
  return fs
528
525
  .readdirSync(componentRoot, { withFileTypes: true })
529
526
  .filter((entry) => entry.isDirectory())
@@ -707,7 +704,6 @@ const invalidateKnownResolutionCacheEntries = (filePath) => {
707
704
  const compileTwigTemplate = (filePath, options, cache = compileCache) => {
708
705
  const absoluteFilePath = resolve(filePath);
709
706
  knownTwigFiles.add(absoluteFilePath);
710
- // eslint-disable-next-line security/detect-non-literal-fs-filename
711
707
  const { mtimeMs } = fs.statSync(absoluteFilePath);
712
708
  const cached = cache.get(absoluteFilePath);
713
709
  if (cached?.mtimeMs === mtimeMs) {
@@ -718,7 +714,6 @@ const compileTwigTemplate = (filePath, options, cache = compileCache) => {
718
714
  registerTwigExtensions(compilerTwig);
719
715
  registerConfiguredTwigExtensions(compilerTwig, options);
720
716
 
721
- // eslint-disable-next-line security/detect-non-literal-fs-filename
722
717
  const source = fs.readFileSync(absoluteFilePath, 'utf8');
723
718
  const compileOptions = {
724
719
  allowInlineIncludes: true,
@@ -1070,7 +1065,6 @@ export function emulsifyTwigModulePlugin(options) {
1070
1065
  const absoluteSourcePath = resolve(sourcePath);
1071
1066
  addDependencyImporter(absoluteSourcePath, sourceFilePath);
1072
1067
  this.addWatchFile(absoluteSourcePath);
1073
- // eslint-disable-next-line security/detect-non-literal-fs-filename
1074
1068
  const sourceText = fs.readFileSync(absoluteSourcePath, 'utf8');
1075
1069
 
1076
1070
  return unique([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emulsify/core",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "Bundled tooling for Storybook development + Vite Build",
5
5
  "keywords": [
6
6
  "component library",
@@ -169,6 +169,7 @@
169
169
  "@storybook/react-vite": "^10.1.4",
170
170
  "@vituum/vite-plugin-twig": "^1.1.0",
171
171
  "autoprefixer": "^10.4.21",
172
+ "axe-core": "^4.11.4",
172
173
  "babel-preset-minify": "^0.5.2",
173
174
  "concurrently": "^9.2.1",
174
175
  "eslint": "^9.39.4",
package/scripts/a11y.js CHANGED
@@ -62,7 +62,6 @@ const applyProjectA11yConfig = (config = {}) => {
62
62
  * @returns {void}
63
63
  */
64
64
  const printHelp = () => {
65
- // eslint-disable-next-line no-console
66
65
  console.log(
67
66
  [
68
67
  'Usage: node scripts/a11y.js [options]',
@@ -128,7 +127,6 @@ const logIssue = ({ type: severity, message, context, selector }) => {
128
127
  `selector: ${selector}`,
129
128
  '',
130
129
  ];
131
- // eslint-disable-next-line no-console
132
130
  console.log(lines.join('\n'));
133
131
  };
134
132
 
@@ -142,11 +140,9 @@ const logReport = ({ issues, pageUrl }) => {
142
140
  const hasIssues = validIssues.length > 0;
143
141
 
144
142
  if (hasIssues) {
145
- // eslint-disable-next-line no-console
146
143
  console.log(`Issues found in component: ${pageUrl}`);
147
144
  validIssues.forEach(logIssue);
148
145
  } else {
149
- // eslint-disable-next-line no-console
150
146
  console.log(`No issues found in component: ${pageUrl}`);
151
147
  }
152
148
 
@@ -13,6 +13,47 @@ import { getTwigTagDefinitions } from './tag-map.js';
13
13
  */
14
14
  const registeredTwigInstances = new WeakSet();
15
15
 
16
+ /**
17
+ * PHP-style boolean conversion used by Twig.js truthiness checks.
18
+ *
19
+ * Twig.js normally provides this through `Twig.lib.boolval`, but dependency
20
+ * optimizer interop can drop the helper while leaving the rest of Twig usable.
21
+ *
22
+ * @param {*} value - Value to coerce using PHP/Twig truthiness rules.
23
+ * @returns {boolean} TRUE when the value should be truthy in Twig.
24
+ */
25
+ function phpBoolval(value) {
26
+ return (
27
+ value !== false &&
28
+ value !== 0 &&
29
+ value !== '' &&
30
+ value !== '0' &&
31
+ !(Array.isArray(value) && value.length === 0) &&
32
+ value !== null &&
33
+ typeof value !== 'undefined'
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Ensure Twig.js has the internal boolean helper required by conditionals.
39
+ *
40
+ * @param {Object} Twig - Twig.js module or compatible extension target.
41
+ * @returns {Object} The same Twig instance after compatibility patching.
42
+ */
43
+ function ensureTwigBoolval(Twig) {
44
+ Twig.extend((InternalTwig) => {
45
+ if (!InternalTwig.lib) {
46
+ InternalTwig.lib = {};
47
+ }
48
+
49
+ if (typeof InternalTwig.lib.boolval !== 'function') {
50
+ InternalTwig.lib.boolval = phpBoolval;
51
+ }
52
+ });
53
+
54
+ return Twig;
55
+ }
56
+
16
57
  /**
17
58
  * Register native Emulsify Twig functions and logic tags with Twig.js.
18
59
  *
@@ -32,6 +73,8 @@ export function registerTwigExtensions(Twig) {
32
73
  );
33
74
  }
34
75
 
76
+ ensureTwigBoolval(Twig);
77
+
35
78
  if (registeredTwigInstances.has(Twig)) {
36
79
  return Twig;
37
80
  }
@@ -33,7 +33,6 @@ export function mergePreviewParameters(defaults = {}, overrides = {}) {
33
33
  if (value === undefined) continue;
34
34
 
35
35
  // Storybook parameter keys are intentionally dynamic.
36
- // eslint-disable-next-line security/detect-object-injection
37
36
  const current = merged[key];
38
37
  const nextValue =
39
38
  isPlainObject(current) && isPlainObject(value)
@@ -41,7 +40,6 @@ export function mergePreviewParameters(defaults = {}, overrides = {}) {
41
40
  : value;
42
41
 
43
42
  // Storybook parameter keys are intentionally dynamic.
44
- // eslint-disable-next-line security/detect-object-injection
45
43
  merged[key] = nextValue;
46
44
  }
47
45
 
@@ -30,7 +30,6 @@ const ENV = (typeof __EMULSIFY_ENV__ !== 'undefined' && __EMULSIFY_ENV__) || {};
30
30
  function resolveFromMap(map, candidates) {
31
31
  for (const key of candidates) {
32
32
  // Vite glob map keys are generated from static Storybook patterns.
33
- // eslint-disable-next-line security/detect-object-injection
34
33
  const value = map[key];
35
34
  if (value) {
36
35
  return value.default ?? value;
@@ -114,7 +113,6 @@ function findGlobEntry(map, candidates) {
114
113
  for (const key of candidates) {
115
114
  if (Object.hasOwnProperty.call(map, key)) {
116
115
  // Vite glob map keys are generated from static Storybook patterns.
117
- // eslint-disable-next-line security/detect-object-injection
118
116
  return { key, value: map[key] };
119
117
  }
120
118
  }
@@ -240,7 +238,6 @@ export function createTwigResolver({
240
238
  candidateKeysForReference: (name) => candidateKeysForReference(name, env),
241
239
  resolveTemplate(name) {
242
240
  // Direct lookups support callers that already resolved a Vite glob key.
243
- // eslint-disable-next-line security/detect-object-injection
244
241
  const direct = modules[name];
245
242
  if (direct) {
246
243
  return direct.default ?? direct;