at-builder 1.1.9 → 1.2.0

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm run build:prod:*)"
5
+ ],
6
+ "deny": []
7
+ }
8
+ }
@@ -1,22 +1,25 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.getENV = exports.setupEnv = exports.getHelpInfo = exports.getVersion = void 0;
4
- const path = require("path");
5
- const fs = require("fs");
6
- var clc = require("cli-color");
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const cli_color_1 = __importDefault(require("cli-color"));
7
10
  /**
8
11
  * Reads and returns the version from package.json
9
12
  * @returns {Promise<string>} The version of the package
10
13
  */
11
14
  const getVersion = async () => {
12
- const packageJSONPath = path.resolve(__dirname, "../../package.json");
15
+ const packageJSONPath = path_1.default.resolve(__dirname, "../../package.json");
13
16
  try {
14
- const content = await fs.promises.readFile(packageJSONPath, { encoding: "utf8" });
17
+ const content = await fs_1.default.promises.readFile(packageJSONPath, { encoding: "utf8" });
15
18
  const config = JSON.parse(content);
16
19
  return config.version;
17
20
  }
18
21
  catch (error) {
19
- console.error(clc.red("Error reading package.json or parsing JSON"));
22
+ console.error(cli_color_1.default.red("Error reading package.json or parsing JSON"));
20
23
  process.exit(1); // Exit the process if package.json is not found or malformed
21
24
  }
22
25
  };
@@ -31,11 +34,11 @@ exports.getVersion = exports.getVersion;
31
34
  * @returns {string} The formatted text
32
35
  */
33
36
  const formatText = (text, color = "white", bold = false, underline = false) => {
34
- let formattedText = clc[color](text);
37
+ let formattedText = cli_color_1.default[color](text);
35
38
  if (bold)
36
- formattedText = clc.bold(formattedText);
39
+ formattedText = cli_color_1.default.bold(formattedText);
37
40
  if (underline)
38
- formattedText = clc.underline(formattedText);
41
+ formattedText = cli_color_1.default.underline(formattedText);
39
42
  return formattedText;
40
43
  };
41
44
  /**
@@ -69,9 +72,9 @@ For more information on a specific command, run \`atb <command> --help\`
69
72
  exports.getHelpInfo = getHelpInfo;
70
73
  const setupEnv = async (basePath) => {
71
74
  // Path to the .env file
72
- const envPath = path.join(basePath, '.env');
75
+ const envPath = path_1.default.join(basePath, '.env');
73
76
  // Check if the .env file exists
74
- if (!fs.existsSync(envPath)) {
77
+ if (!fs_1.default.existsSync(envPath)) {
75
78
  // Define the content of the .env file
76
79
  const envContent = `
77
80
  ACTIVITY_FOLDER_NAME=""
@@ -83,7 +86,7 @@ NODE_ENV="development"
83
86
  VERBOSE=false
84
87
  `;
85
88
  // Write the content to the .env file
86
- fs.writeFileSync(envPath, envContent.trim(), 'utf8');
89
+ fs_1.default.writeFileSync(envPath, envContent.trim(), 'utf8');
87
90
  console.log('.env file created successfully!');
88
91
  }
89
92
  else {
@@ -94,14 +97,14 @@ VERBOSE=false
94
97
  exports.setupEnv = setupEnv;
95
98
  const createWatchConfig = (basePath) => {
96
99
  // Path to the watch-config.json file
97
- const watchConfigPath = path.join(basePath, 'watch-config.json');
100
+ const watchConfigPath = path_1.default.join(basePath, 'watch-config.json');
98
101
  // Default content for watch-config.json
99
102
  const watchConfigContent = {
100
103
  "VARIATION": "Variation-1"
101
104
  };
102
105
  // Check if the watch-config.json file already exists
103
- if (!fs.existsSync(watchConfigPath)) {
104
- fs.writeFileSync(watchConfigPath, JSON.stringify(watchConfigContent, null, 2), 'utf8');
106
+ if (!fs_1.default.existsSync(watchConfigPath)) {
107
+ fs_1.default.writeFileSync(watchConfigPath, JSON.stringify(watchConfigContent, null, 2), 'utf8');
105
108
  console.log('watch-config.json file created successfully!');
106
109
  }
107
110
  else {
@@ -114,14 +117,14 @@ const createWatchConfig = (basePath) => {
114
117
  * @returns {Promise<Object>} Object containing environment variables
115
118
  */
116
119
  const getENV = async (basePath) => {
117
- const envPath = path.join(basePath, '.env');
120
+ const envPath = path_1.default.join(basePath, '.env');
118
121
  try {
119
122
  // Check if .env file exists
120
- if (!fs.existsSync(envPath)) {
123
+ if (!fs_1.default.existsSync(envPath)) {
121
124
  throw new Error('.env file not found');
122
125
  }
123
126
  // Read the .env file
124
- const envContent = await fs.promises.readFile(envPath, { encoding: 'utf8' });
127
+ const envContent = await fs_1.default.promises.readFile(envPath, { encoding: 'utf8' });
125
128
  // Parse the .env file content
126
129
  const envVars = {};
127
130
  envContent.split('\n').forEach(line => {
@@ -139,7 +142,7 @@ const getENV = async (basePath) => {
139
142
  return envVars;
140
143
  }
141
144
  catch (error) {
142
- console.error(clc.red(`Error reading .env file: ${error.message}`));
145
+ console.error(cli_color_1.default.red(`Error reading .env file: ${error.message}`));
143
146
  throw error;
144
147
  }
145
148
  };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * A custom Webpack plugin to wrap bundled assets with a header and footer.
5
+ */
6
+ class CustomWrapperPlugin {
7
+ /**
8
+ * Creates an instance of CustomWrapperPlugin.
9
+ * @param {object} options - The plugin options.
10
+ * @param {RegExp} options.test - A regular expression to match file paths that should be wrapped.
11
+ * @param {string|Function} [options.header=''] - The content to prepend to the file. Can be a string or a function that returns a string.
12
+ * @param {string|Function} [options.footer=''] - The content to append to the file. Can be a string or a function that returns a string.
13
+ */
14
+ constructor(options) {
15
+ this.options = options || {};
16
+ }
17
+
18
+ /**
19
+ * Applies the plugin to the Webpack compiler.
20
+ * @param {import('webpack').Compiler} compiler - The Webpack compiler instance.
21
+ */
22
+ apply(compiler) {
23
+ const pluginName = this.constructor.name;
24
+ const { webpack } = compiler;
25
+ const { Compilation } = webpack;
26
+
27
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
28
+ compilation.hooks.processAssets.tap(
29
+ {
30
+ name: pluginName,
31
+ stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE
32
+ },
33
+ (assets) => {
34
+ for (const pathname in assets) {
35
+ if (this.options.test && this.options.test.test(pathname)) {
36
+ const asset = compilation.getAsset(pathname);
37
+ const source = asset.source.source();
38
+ const args = { hash: compilation.hash };
39
+
40
+ const wrappedSource =
41
+ (typeof this.options.header === 'function'
42
+ ? this.options.header(source, args)
43
+ : this.options.header || '') +
44
+ source +
45
+ (typeof this.options.footer === 'function'
46
+ ? this.options.footer(source, args)
47
+ : this.options.footer || '');
48
+
49
+ compilation.updateAsset(
50
+ pathname,
51
+ new webpack.sources.RawSource(wrappedSource)
52
+ );
53
+ }
54
+ }
55
+ }
56
+ );
57
+ });
58
+ }
59
+ }
60
+
61
+ module.exports = CustomWrapperPlugin;
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "at-builder",
3
- "version": "1.1.9",
3
+ "version": "1.2.0",
4
4
  "main": "bin/index.js",
5
5
  "bin": {
6
6
  "atb": "bin/index.js"
7
7
  },
8
+ "type": "module",
8
9
  "scripts": {
9
10
  "build": "tsc -w",
10
- "build-prod": "cross-env NODE_ENV=production webpack",
11
+ "build:prod": "cross-env NODE_ENV=production webpack",
11
12
  "dev": "webpack -- -w",
12
13
  "dev-puppeteer": "webpack -- -w & npm run puppeteer",
13
14
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -62,6 +63,7 @@
62
63
  "@babel/runtime": "^7.26.7",
63
64
  "@eslint/js": "^9.20.0",
64
65
  "globals": "^15.15.0",
66
+ "kleur": "^4.1.5",
65
67
  "typescript-eslint": "^8.24.1"
66
68
  },
67
69
  "repository": {
package/plopfile.js CHANGED
@@ -1,6 +1,6 @@
1
- const components = require("./.plop/generators/components");
1
+ import components from "./.plop/generators/components";
2
2
 
3
3
 
4
- module.exports = function (plop) {
4
+ export default function (plop) {
5
5
  plop.setGenerator('components', components );
6
6
  };
package/puppeteer.js CHANGED
@@ -1,9 +1,10 @@
1
- const path = require('path');
2
- const { executionPath } = process.env;
3
- require('dotenv').config({ path: path.join(executionPath, ".env") });
4
- const puppeteer = require('puppeteer');
5
- const fs = require('fs');
6
- const chokidar = require('chokidar');
1
+ import dotenv from 'dotenv';
2
+ import path from 'path';
3
+ import puppeteer from 'puppeteer';
4
+ import fs from 'fs';
5
+ import chokidar from 'chokidar';
6
+ import { executionPath } from process.env;
7
+ dotenv.config({ path: path.join(executionPath, ".env") });
7
8
  //const watchConfig = "./../watch-config.json";
8
9
 
9
10
  let watchConfig = '';
@@ -1,6 +1,6 @@
1
- const path = require("path");
2
- const fs = require("fs");
3
- var clc = require("cli-color");
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import clc from "cli-color";
4
4
 
5
5
  /**
6
6
  * Reads and returns the version from package.json
package/webpack.config.js CHANGED
@@ -1,42 +1,27 @@
1
- const { executionPath: PWD } = process.env;
2
- const path = require('path');
3
- require('dotenv').config({ path: path.join(PWD, ".env") });
4
- const fs = require('fs');
5
- const WrapperPlugin = require('wrapper-webpack-plugin');
6
- const TerserPlugin = require("terser-webpack-plugin");
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { createRequire } from 'module';
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const dotenv = require('dotenv');
7
+ const TerserPlugin = require('terser-webpack-plugin');
7
8
  const ESLintPlugin = require('eslint-webpack-plugin');
8
9
  const postcssPresetEnv = require('postcss-preset-env');
9
- const AdobeTargetBuildGeneratorPlugin = require('./lib/at-build-generator');
10
- // const commander = require("commander");
10
+ const AdobeTargetBuildGeneratorPlugin = require('./lib/at-build-generator.cjs');
11
11
 
12
- console.log(process.env.ACTIVITY_FOLDER_NAME)
12
+ const PWD = process.env.executionPath || process.cwd();
13
+ dotenv.config({ path: path.join(PWD, ".env") });
13
14
 
14
15
  /**
15
16
  * Load from env files
16
17
  */
17
18
  const PRODUCTION = "production";
18
19
 
19
- let environment = "development";
20
-
21
20
  /**
22
21
  * Set current running environment
23
22
  */
24
- if (process.env.NODE_ENV === PRODUCTION) {
25
- console.log("\x1b[36m%s\x1b[0m", "------ Running Production Build ------")
26
- environment = PRODUCTION;
27
- } else {
28
- console.log("\x1b[36m%s\x1b[0m", "------ Running Development Build ------")
29
- }
23
+ let environment = process.env.NODE_ENV === PRODUCTION ? PRODUCTION : "development";
30
24
 
31
- // /**
32
- // * commander
33
- // */
34
- // commander
35
- // .version("1.0.0", "-v, --version")
36
- // .option("-p, --path <value>", "Overwriting value.", process.cwd())
37
- // .parse(process.argv);
38
-
39
- // const options = commander.opts();
40
25
 
41
26
  /**
42
27
  * Update folder name here if wanted to skip while transpilation
@@ -74,7 +59,7 @@ var traversePath = function (dir) {
74
59
  if (!fileName.startsWith("V")) {
75
60
  return;
76
61
  }
77
- console.log(++count + ")", "\x1b[36m", fileName, "\x1b[0m");
62
+ // console.log(++count + ")", "\x1b[36m", fileName, "\x1b[0m", `\n Build path: ${file}/dist/build.js`);
78
63
  var res = traversePath(file);
79
64
  // Process only if have child nodes
80
65
  if (res && res.length)
@@ -88,11 +73,9 @@ var traversePath = function (dir) {
88
73
  return results;
89
74
  }
90
75
 
91
- console.log("\x1b[33m%s\x1b[0m", `${process.env.ACTIVITY_FOLDER_NAME}`);
92
-
93
- const filepath = PWD;
76
+ // console.log("\x1b[33m%s\x1b[0m", `${process.env.ACTIVITY_FOLDER_NAME}`);
94
77
 
95
- var files = traversePath(path.join(filepath, 'Activities', process.env.ACTIVITY_FOLDER_NAME).toString());
78
+ var files = traversePath(path.join(PWD, 'Activities', process.env.ACTIVITY_FOLDER_NAME).toString());
96
79
 
97
80
  //Create entry object which is needed for webpack config
98
81
  var entries = {};
@@ -100,17 +83,14 @@ files.forEach((file) => {
100
83
  var foldername = file.split("/").pop();
101
84
  let distFolderPath = file.replace("/" + foldername, "/dist");
102
85
 
103
- console.log("distfolder", distFolderPath)
104
86
  if (fs.existsSync(distFolderPath) && environment == PRODUCTION) {
105
- fs.rmdirSync(distFolderPath, { recursive: true });
106
- // fs.rm(distFolderPath, console.log);
87
+ fs.rmSync(distFolderPath, { recursive: true, force: true });
107
88
  }
108
89
  var newPath = `${distFolderPath}/build`;
109
90
  // Replace path with absolute path before making a key of the entries object
110
- entries[newPath.replace(PWD, "")] = file;
91
+ entries[newPath.replace(PWD, "").replace(/^\//, '')] = file;
111
92
  });
112
93
 
113
- console.log("++++ENTRIES++++", entries);
114
94
 
115
95
  const isProdMode = environment == PRODUCTION;
116
96
 
@@ -119,30 +99,110 @@ const plugins = [
119
99
  extensions: "js",
120
100
  outputReport: true,
121
101
  fix: true
122
- }),
102
+ })
103
+ ];
123
104
 
124
- new WrapperPlugin({
125
- test: /\.js$/,
126
- header: function (_, args) {
127
- //Handle target issue for reloading the styles.
105
+ // Modern replacement for WrapperPlugin using webpack's BannerPlugin and custom plugin
106
+ plugins.push({
107
+ apply: (compiler) => {
108
+ compiler.hooks.compilation.tap('ModernWrapperPlugin', (compilation) => {
109
+ compilation.hooks.processAssets.tap(
110
+ {
111
+ name: 'ModernWrapperPlugin',
112
+ stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
113
+ },
114
+ async (assets) => {
115
+ const crypto = await import('crypto');
116
+ const webpackSources = await import('webpack-sources');
117
+
118
+ for (const assetName of Object.keys(assets)) {
119
+ if (assetName.endsWith('.js')) {
120
+ const asset = assets[assetName];
121
+ const source = asset.source();
122
+
123
+ // Generate unique test ID
124
+ const preFix = process.env.TRACK || 'TargetBuild';
125
+ const hash = crypto.createHash('md5').update(assetName + source).digest('hex').substring(0, 8);
126
+ const uniqueTestId = `${preFix}_${hash}`;
127
+
128
+ const wrappedSource = `(function(){
129
+ if(!window.${uniqueTestId}){
130
+ //# sourceURL=${uniqueTestId}.js
131
+ ${source}
132
+ window.${uniqueTestId} = true;
133
+ }
134
+ })();`;
128
135
 
129
- const preFix = process.env.TRACK ? process.env.TRACK : 'TargetBuild';
130
- let _uniqueTestId = `${preFix}_${args.hash}`;
131
- args._uniqueTestId = _uniqueTestId;
132
- return `(function(){
133
- if(!window.${_uniqueTestId}){
134
- //# sourceURL=${_uniqueTestId}.js
135
- `;
136
- },
137
- footer: function (_, args) {
138
- return `\n
139
- window.${args._uniqueTestId} = true;
136
+ compilation.updateAsset(assetName, new webpackSources.RawSource(wrappedSource));
137
+ }
138
+ }
139
+ }
140
+ );
141
+ });
142
+ }
143
+ });
144
+
145
+ // Build Summary Plugin
146
+ plugins.push({
147
+ apply: (compiler) => {
148
+ compiler.hooks.done.tap('BuildSummaryPlugin', async (stats) => {
149
+ let kleur;
150
+ try {
151
+ const kleurModule = await import('kleur');
152
+ kleur = kleurModule.default;
153
+ } catch (e) {
154
+ // Fallback if kleur is not available
155
+ kleur = {
156
+ cyan: (text) => `\x1b[36m${text}\x1b[0m`,
157
+ gray: (text) => `\x1b[90m${text}\x1b[0m`,
158
+ bold: (text) => `\x1b[1m${text}\x1b[0m`,
159
+ yellow: (text) => `\x1b[33m${text}\x1b[0m`,
160
+ green: (text) => `\x1b[32m${text}\x1b[0m`,
161
+ blue: (text) => `\x1b[34m${text}\x1b[0m`,
162
+ magenta: (text) => `\x1b[35m${text}\x1b[0m`
163
+ };
164
+ kleur.gray.underline = (text) => `\x1b[90m\x1b[4m${text}\x1b[0m`;
140
165
  }
141
- })();`
142
- },
143
- afterOptimization: true,
144
- })
145
- ];
166
+ const project = process.env.ACTIVITY_FOLDER_NAME || 'Project';
167
+ const info = stats.toJson({ all: false, assets: true, modules: true, version: true, timings: true });
168
+
169
+ const buildModules = Object.entries(entries).map(([out, src], idx) => {
170
+ const parts = out.split(path.sep);
171
+ let name = parts[parts.length - 3] || `Module-${idx + 1}`;
172
+ name = name.replace(/dist|build/gi, '').replace(/[-_]/g, '').trim() || `Module-${idx + 1}`;
173
+ return { idx: idx + 1, name, path: out };
174
+ });
175
+
176
+ const duration = (stats.endTime - stats.startTime) / 1000;
177
+ const mode = environment === PRODUCTION ? 'Production' : 'Development';
178
+
179
+ console.log(kleur.cyan(`\n📦 ${mode} Build Summary`));
180
+ console.log(kleur.gray('├── ') + kleur.bold('Activity: ') + kleur.yellow(project));
181
+ console.log(kleur.gray('├── ') + kleur.bold('Modules: '));
182
+
183
+ buildModules.forEach((m, i) => {
184
+ const assetPath = m.path.replace(/^\/+/, '');
185
+ const assetName = assetPath + '.js';
186
+ const asset = info.assets.find(a => a.name === assetName);
187
+ const sizeKB = asset ? (asset.size / 1024).toFixed(2) : '0.00';
188
+ const absPath = path.resolve(PWD, assetPath.replace(/\/build$/, ""), environment === PRODUCTION ? 'at-build.html' : 'build.js');
189
+ const atBuildLink = makeClickableLink(absPath, 1, 1);
190
+
191
+ const prefix = (i === buildModules.length - 1) ? '└──' : '├──';
192
+ console.log(kleur.gray(`│ ${prefix} `) + kleur.green(`${m.name} (${sizeKB} KB)`));
193
+ if (environment === PRODUCTION) {
194
+ console.log(`🔗 ${kleur.blue(`${m.name} Production Build`)} 🔗`);
195
+ console.log(`${kleur.gray().underline(atBuildLink)}`);
196
+ }
197
+ });
198
+ console.log(kleur.gray('│ '));
199
+ console.log(kleur.gray('├── ') + kleur.bold('Assets: ') + kleur.magenta(info.assets.length));
200
+ console.log(kleur.gray('├── ') + kleur.bold('Modules: ') + kleur.magenta(info.modules.length));
201
+ console.log(kleur.gray('├── ') + kleur.bold('Webpack: ') + kleur.magenta(info.version));
202
+ console.log(kleur.gray('├── ') + kleur.bold('Duration: ') + kleur.magenta(duration.toFixed(2) + 's'));
203
+ });
204
+ }
205
+ });
146
206
 
147
207
  if (isProdMode) {
148
208
  plugins.push(new AdobeTargetBuildGeneratorPlugin({
@@ -150,7 +210,7 @@ if (isProdMode) {
150
210
  fileName: "at-build",
151
211
  fileType: "html",
152
212
  header: function (source, outputPath) {
153
- return `<script targetExp="${process.env.ACTIVITY_FOLDER_NAME}" variation="${(outputPath.split(path.sep)).slice(-3, -1)[0]}" type="application/javascript">`;
213
+ return `<script targetExp="${process.env.ACTIVITY_FOLDER_NAME}" variation="${(outputPath.split(path.sep)).slice(-3, -1)[1]}" type="application/javascript">`;
154
214
  },
155
215
  footer: function () {
156
216
  return `</script>`;
@@ -159,8 +219,10 @@ if (isProdMode) {
159
219
  }
160
220
 
161
221
  // Webpack config object with dynamic entry points
162
- module.exports = {
163
- ...(environment !== PRODUCTION && { devtool: 'inline-source-map' }),
222
+ export default {
223
+ devtool: false,
224
+ // devtool: 'source-map', // generate source-map in build directory
225
+ //...(environment !== PRODUCTION && { devtool: 'inline-source-map' }),
164
226
  mode: environment,
165
227
  entry: {
166
228
  ...entries
@@ -184,6 +246,9 @@ module.exports = {
184
246
  {
185
247
  test: /\.js$/,
186
248
  exclude: /node_modules/,
249
+ resolve: {
250
+ fullySpecified: false
251
+ },
187
252
  use: {
188
253
  loader: 'babel-loader',
189
254
  options: {
@@ -222,10 +287,14 @@ module.exports = {
222
287
  {
223
288
  loader: 'style-loader',
224
289
  options: {
225
- injectType: 'singletonStyleTag',
290
+ injectType: 'singletonStyleTag', // styleTag | singletonStyleTag | lazyStyleTag | lazySingletonStyleTag | linkTag
226
291
  attributes: {
227
- id: "ddo-styles" //TO_DO this should load from env or watch config
292
+ id: process.env.STYLE_ID || "ddo-styles"
228
293
  }
294
+ // sourceMap: environment !== PRODUCTION
295
+ // insert: "body" | (element) => {}
296
+ // insert:(element) => {}
297
+ // esModule: false, default is true - ES modules is beneficial, like in the case of module concatenation and tree shaking.
229
298
  }
230
299
  },
231
300
  {
@@ -251,6 +320,9 @@ module.exports = {
251
320
  },
252
321
  {
253
322
  loader: "sass-loader",
323
+ options: {
324
+ api: 'modern'
325
+ }
254
326
  }
255
327
  ]
256
328
 
@@ -271,7 +343,7 @@ module.exports = {
271
343
  comments: /# sourceURL=/i
272
344
  },
273
345
  compress: {
274
- drop_console: isProdMode,
346
+ drop_console: environment === PRODUCTION,
275
347
  }
276
348
  },
277
349
  extractComments: false
@@ -280,10 +352,10 @@ module.exports = {
280
352
  ]
281
353
  },
282
354
  plugins: plugins,
283
- stats: 'minimal',
284
- performance: {
285
- maxAssetSize: 1024000,
286
- maxEntrypointSize: 1024000,
287
- hints: "warning"
288
- }
289
- };
355
+ stats: 'minimal'
356
+ };
357
+
358
+ function makeClickableLink(fp, line = 1, col = 1) {
359
+ const uri = `${fp}:${line}:${col}`;
360
+ return `${uri}`;
361
+ }
File without changes