@bigbinary/neeto-commons-frontend 4.14.4 → 4.14.5

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.
@@ -13,6 +13,7 @@ const postcss = require("postcss");
13
13
  const sass = require("sass");
14
14
 
15
15
  const alias = require("./alias.js");
16
+ const { atomsImportsPlugin } = require("./plugins/atomsImports.js");
16
17
  const { babelPlugin } = require("./plugins/babel.js");
17
18
  const { entrypointPlugin } = require("./plugins/entrypoint.js");
18
19
  const {
@@ -51,6 +52,8 @@ const config = {
51
52
  assetNames: "assets/[name]-[hash].digested",
52
53
  chunkNames: "chunks/[name]-[hash].digested",
53
54
  plugins: [
55
+ // Before stripSourcemapsPlugin: both load @bigbinary files.
56
+ atomsImportsPlugin(),
54
57
  stripSourcemapsPlugin(),
55
58
  rails(),
56
59
  entrypointPlugin(isWatchMode),
@@ -0,0 +1,318 @@
1
+ // Rewrites imports from the neeto-atoms barrels to the modules that define
2
+ // each name, in app code and in every @bigbinary package.
3
+ //
4
+ // esbuild assigns a module to the chunks of every entry that can reach it. The
5
+ // barrels (`@bigbinary/neeto-atoms`, `/primitives`, `/formik`) re-export the
6
+ // whole library, so once any module on a page's import path imports a barrel,
7
+ // every component used anywhere in the product lands in that page's chunks.
8
+ // Importing each name from its defining module keeps a component in the
9
+ // chunks of the pages that use it.
10
+ //
11
+ // The map is read from the installed barrels at build time, so new atoms
12
+ // exports need no change here. Anything the rewrite can't map (namespace or
13
+ // default imports, unknown names, files that fail to parse) is left as it is.
14
+
15
+ /* eslint-disable @bigbinary/neeto/no-dangling-constants */
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const parser = require("@babel/parser");
20
+
21
+ const { stripSourcemapUrl, SOURCEMAP_SKIP_RE } = require("./stripSourcemaps");
22
+
23
+ const PACKAGE_NAME = "@bigbinary/neeto-atoms";
24
+ const SPECIFIER_RE = /^(@bigbinary\/neeto-atoms|neetoatoms)(\/[a-z]+)?$/;
25
+ const MENTION_RE = /neeto-?atoms/;
26
+ const INLINE_NAMESPACE = "neeto-atoms-inline";
27
+ const BARE_PREFIX = "neeto-atoms-bare:";
28
+
29
+ const parse = (code, jsx) =>
30
+ parser.parse(code, {
31
+ sourceType: "module",
32
+ plugins: jsx ? ["jsx"] : [],
33
+ });
34
+
35
+ const findPackageDir = cwd => {
36
+ try {
37
+ const entry = require.resolve(PACKAGE_NAME, { paths: [cwd] });
38
+ let dir = path.dirname(entry);
39
+ while (dir !== path.dirname(dir)) {
40
+ const manifestPath = path.join(dir, "package.json");
41
+ if (fs.existsSync(manifestPath)) {
42
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
43
+ if (manifest.name === PACKAGE_NAME) return { dir, manifest };
44
+ }
45
+ dir = path.dirname(dir);
46
+ }
47
+ } catch {
48
+ return null;
49
+ }
50
+
51
+ return null;
52
+ };
53
+
54
+ // atoms marks only CSS as having side effects, so an import of a barrel
55
+ // that binds nothing can be dropped.
56
+ const hasSideEffectFreeJs = ({ sideEffects }) =>
57
+ sideEffects === false ||
58
+ (Array.isArray(sideEffects) &&
59
+ sideEffects.every(pattern => /\.s?css$/.test(pattern)));
60
+
61
+ const readBarrel = file => {
62
+ const code = fs.readFileSync(file, "utf8");
63
+ const dir = path.dirname(file);
64
+ const names = {};
65
+ const reexportRanges = [];
66
+
67
+ parse(code, false).program.body.forEach(node => {
68
+ if (node.type === "ExportAllDeclaration") {
69
+ reexportRanges.push([node.start, node.end]);
70
+
71
+ return;
72
+ }
73
+
74
+ if (node.type !== "ExportNamedDeclaration") return;
75
+
76
+ if (!node.source) {
77
+ // Defined in the barrel itself, served from a module of its own.
78
+ node.specifiers.forEach(specifier => {
79
+ names[specifier.exported.name] = {
80
+ target: `${INLINE_NAMESPACE}:${file}`,
81
+ imported: specifier.exported.name,
82
+ };
83
+ });
84
+
85
+ (node.declaration?.declarations ?? []).forEach(({ id }) => {
86
+ if (id.type === "Identifier") {
87
+ names[id.name] = {
88
+ target: `${INLINE_NAMESPACE}:${file}`,
89
+ imported: id.name,
90
+ };
91
+ }
92
+ });
93
+
94
+ return;
95
+ }
96
+
97
+ reexportRanges.push([node.start, node.end]);
98
+ const source = node.source.value;
99
+ const target = source.startsWith(".")
100
+ ? path.resolve(dir, source)
101
+ : `${BARE_PREFIX}${JSON.stringify([dir, source])}`;
102
+
103
+ node.specifiers.forEach(specifier => {
104
+ if (specifier.type !== "ExportSpecifier") return;
105
+
106
+ names[specifier.exported.name] = {
107
+ target,
108
+ imported: specifier.local.name,
109
+ };
110
+ });
111
+ });
112
+
113
+ // The barrel's own code without its re-exports, for the inline exports.
114
+ let inlineCode = code;
115
+ [...reexportRanges]
116
+ .sort((a, b) => b[0] - a[0])
117
+ .forEach(([start, end]) => {
118
+ inlineCode = inlineCode.slice(0, start) + inlineCode.slice(end);
119
+ });
120
+
121
+ return { names, inlineCode };
122
+ };
123
+
124
+ const createRewriter = ({ dir, manifest }) => {
125
+ const barrels = {};
126
+ const inlineModules = {};
127
+ Object.entries(manifest.exports ?? {}).forEach(([subpath, conditions]) => {
128
+ const file =
129
+ typeof conditions === "string" ? conditions : conditions?.import;
130
+ if (!file || subpath.includes("*") || !file.endsWith(".js")) return;
131
+
132
+ const absolute = path.join(dir, file);
133
+ if (!fs.existsSync(absolute)) return;
134
+
135
+ try {
136
+ const { names, inlineCode } = readBarrel(absolute);
137
+ barrels[subpath.replace(/^\./, "")] = names;
138
+ inlineModules[absolute] = inlineCode;
139
+ } catch {
140
+ // A barrel we can't read keeps being imported as it is.
141
+ }
142
+ });
143
+ const dropBareImports = hasSideEffectFreeJs(manifest);
144
+
145
+ const rewrite = (code, { jsx = false } = {}) => {
146
+ if (!MENTION_RE.test(code)) return code;
147
+
148
+ let ast;
149
+ try {
150
+ ast = parse(code, jsx);
151
+ } catch {
152
+ return code;
153
+ }
154
+
155
+ const edits = [];
156
+ ast.program.body.forEach(node => {
157
+ const isImport = node.type === "ImportDeclaration";
158
+ const isReexport =
159
+ node.type === "ExportNamedDeclaration" && Boolean(node.source);
160
+ if (!isImport && !isReexport) return;
161
+
162
+ const match = node.source.value.match(SPECIFIER_RE);
163
+ const names = match && barrels[match[2] ?? ""];
164
+ if (!names) return;
165
+
166
+ if (isImport && node.specifiers.length === 0) {
167
+ if (dropBareImports) edits.push({ node, statements: [] });
168
+
169
+ return;
170
+ }
171
+
172
+ const unsupported = node.specifiers.some(
173
+ ({ type }) => type !== "ImportSpecifier" && type !== "ExportSpecifier"
174
+ );
175
+ if (unsupported || node.importKind === "type") return;
176
+
177
+ const byTarget = new Map();
178
+ const unmapped = [];
179
+ node.specifiers.forEach(specifier => {
180
+ const requested = isImport
181
+ ? (specifier.imported.name ?? specifier.imported.value)
182
+ : specifier.local.name;
183
+ const alias = isImport ? specifier.local.name : specifier.exported.name;
184
+ const entry = names[requested];
185
+ if (!entry) {
186
+ unmapped.push(
187
+ requested === alias ? requested : `${requested} as ${alias}`
188
+ );
189
+
190
+ return;
191
+ }
192
+
193
+ if (!byTarget.has(entry.target)) byTarget.set(entry.target, []);
194
+ byTarget.get(entry.target).push(`${entry.imported} as ${alias}`);
195
+ });
196
+
197
+ const keyword = isImport ? "import" : "export";
198
+ const statements = [...byTarget].map(
199
+ ([target, specifiers]) =>
200
+ `${keyword} { ${specifiers.join(", ")} } from ${JSON.stringify(target)};`
201
+ );
202
+ if (unmapped.length > 0) {
203
+ statements.push(
204
+ `${keyword} { ${unmapped.join(", ")} } from ${JSON.stringify(
205
+ node.source.value
206
+ )};`
207
+ );
208
+ }
209
+ edits.push({ node, statements });
210
+ });
211
+
212
+ if (edits.length === 0) return code;
213
+
214
+ // Keep every line where it was, so sourcemaps of the rest of the file
215
+ // stay valid.
216
+ return edits
217
+ .sort((a, b) => b.node.start - a.node.start)
218
+ .reduce((result, { node, statements }) => {
219
+ const original = result.slice(node.start, node.end);
220
+ const newlines = "\n".repeat((original.match(/\n/g) ?? []).length);
221
+
222
+ return (
223
+ result.slice(0, node.start) +
224
+ statements.join(" ") +
225
+ newlines +
226
+ result.slice(node.end)
227
+ );
228
+ }, code);
229
+ };
230
+
231
+ return { rewrite, inlineModules };
232
+ };
233
+
234
+ const rewriters = new Map();
235
+ // Rewritten imports can point at this plugin's virtual modules, so app code
236
+ // is only rewritten when the plugin is part of the build.
237
+ let isPluginInstalled = false;
238
+
239
+ const getRewriter = (cwd = process.cwd()) => {
240
+ if (!rewriters.has(cwd)) {
241
+ const atoms = findPackageDir(cwd);
242
+ rewriters.set(cwd, atoms ? { ...createRewriter(atoms), atoms } : null);
243
+ }
244
+
245
+ return rewriters.get(cwd);
246
+ };
247
+
248
+ const rewriteAtomsImports = (code, options) =>
249
+ getRewriter(options?.cwd)?.rewrite(code, options) ?? code;
250
+
251
+ const rewriteAtomsImportsInAppCode = code =>
252
+ isPluginInstalled ? rewriteAtomsImports(code, { jsx: true }) : code;
253
+
254
+ const atomsImportsPlugin = ({ cwd } = {}) => ({
255
+ name: "atoms-imports",
256
+ setup(build) {
257
+ const rewriter = getRewriter(cwd);
258
+ if (!rewriter) return;
259
+
260
+ isPluginInstalled = true;
261
+
262
+ const atomsDir = `${rewriter.atoms.dir}${path.sep}`;
263
+
264
+ build.onResolve({ filter: new RegExp(`^${INLINE_NAMESPACE}:`) }, args => ({
265
+ path: args.path.slice(INLINE_NAMESPACE.length + 1),
266
+ namespace: INLINE_NAMESPACE,
267
+ }));
268
+
269
+ build.onLoad({ filter: /.*/, namespace: INLINE_NAMESPACE }, args => {
270
+ const contents = rewriter.inlineModules[args.path];
271
+ if (contents === undefined) {
272
+ return { errors: [{ text: `Not a neeto-atoms barrel: ${args.path}` }] };
273
+ }
274
+
275
+ return { contents, resolveDir: path.dirname(args.path), loader: "js" };
276
+ });
277
+
278
+ // Names atoms re-exports from other packages resolve from atoms' folder,
279
+ // as they did through the barrel.
280
+ build.onResolve({ filter: new RegExp(`^${BARE_PREFIX}`) }, args => {
281
+ const [resolveDir, specifier] = JSON.parse(
282
+ args.path.slice(BARE_PREFIX.length)
283
+ );
284
+
285
+ return build.resolve(specifier, { resolveDir, kind: args.kind });
286
+ });
287
+
288
+ build.onLoad(
289
+ { filter: /node_modules\/@bigbinary\/.*\.m?js$/ },
290
+ async args => {
291
+ if (
292
+ args.path.startsWith(atomsDir) ||
293
+ SOURCEMAP_SKIP_RE.test(args.path)
294
+ ) {
295
+ return null;
296
+ }
297
+
298
+ const code = await fs.promises.readFile(args.path, "utf8");
299
+ // Whatever this handler returns, stripSourcemapsPlugin no longer loads,
300
+ // so it strips the sourcemap comment too and each file is read once.
301
+ const contents = stripSourcemapUrl(rewriter.rewrite(code));
302
+ if (contents === code) return null;
303
+
304
+ return {
305
+ contents,
306
+ loader: "js",
307
+ resolveDir: path.dirname(args.path),
308
+ };
309
+ }
310
+ );
311
+ },
312
+ });
313
+
314
+ module.exports = {
315
+ atomsImportsPlugin,
316
+ rewriteAtomsImports,
317
+ rewriteAtomsImportsInAppCode,
318
+ };
@@ -1,12 +1,16 @@
1
1
  const babel = require("@babel/core");
2
2
  const parser = require("@babel/parser");
3
3
 
4
+ const { rewriteAtomsImportsInAppCode } = require("./atomsImports");
5
+
4
6
  const babelPlugin = () => ({
5
7
  name: "babel-plugin",
6
8
  setup(build) {
7
9
  build.onLoad({ filter: /app\/javascript\/.*\.*\.jsx?$/ }, async args => {
8
10
  const fs = require("fs").promises;
9
- const contents = await fs.readFile(args.path, "utf8");
11
+ const contents = rewriteAtomsImportsInAppCode(
12
+ await fs.readFile(args.path, "utf8")
13
+ );
10
14
 
11
15
  const ast = parser.parse(contents, {
12
16
  sourceType: "module",
@@ -23,6 +23,8 @@ const fs = require("fs").promises;
23
23
  const SKIP_RE = /(v2\/)?initializers\/i18n\.js$/;
24
24
  const SOURCEMAP_URL_RE = /\/\/[#@] sourceMappingURL=[^\n]+/gm;
25
25
 
26
+ const stripSourcemapUrl = contents => contents.replace(SOURCEMAP_URL_RE, "");
27
+
26
28
  const stripSourcemapsPlugin = () => ({
27
29
  name: "strip-node-modules-sourcemaps",
28
30
  setup(build) {
@@ -35,7 +37,7 @@ const stripSourcemapsPlugin = () => ({
35
37
  if (!SOURCEMAP_URL_RE.test(contents)) return null;
36
38
 
37
39
  return {
38
- contents: contents.replace(SOURCEMAP_URL_RE, ""),
40
+ contents: stripSourcemapUrl(contents),
39
41
  loader: "js",
40
42
  };
41
43
  }
@@ -43,4 +45,10 @@ const stripSourcemapsPlugin = () => ({
43
45
  },
44
46
  });
45
47
 
46
- module.exports = { stripSourcemapsPlugin };
48
+ // atomsImportsPlugin loads @bigbinary files before this plugin and uses these
49
+ // too, so the two agree on what to skip and strip.
50
+ module.exports = {
51
+ stripSourcemapsPlugin,
52
+ stripSourcemapUrl,
53
+ SOURCEMAP_SKIP_RE: SKIP_RE,
54
+ };
package/dist/.ready CHANGED
@@ -1 +1 @@
1
- Built at 2026-10-23T16:32:29.063Z
1
+ Built at 2026-10-26T09:42:29.086Z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigbinary/neeto-commons-frontend",
3
- "version": "4.14.4",
3
+ "version": "4.14.5",
4
4
  "description": "A package encapsulating common code across neeto projects including initializers, utility functions, common components and hooks and so on.",
5
5
  "repository": "git@github.com:bigbinary/neeto-commons-frontend.git",
6
6
  "author": "Amaljith K <amaljith.k@bigbinary.com>",