@navikt/aksel 7.35.0 → 7.35.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.
@@ -17,7 +17,15 @@ exports.GLOB_IGNORE_PATTERNS = GLOB_IGNORE_PATTERNS;
17
17
  */
18
18
  function getDefaultGlob(ext) {
19
19
  const defaultExt = "js,ts,jsx,tsx,css,scss,less";
20
- return `**/*.{${cleanExtensions(ext !== null && ext !== void 0 ? ext : defaultExt).join(",")}}`;
20
+ const extensions = cleanExtensions(ext !== null && ext !== void 0 ? ext : defaultExt);
21
+ /**
22
+ * Single-item braces are treated as a literal string by some globbing libraries,
23
+ * so we only use them when there are multiple extensions
24
+ */
25
+ if (extensions.length > 1) {
26
+ return `**/*.{${extensions.join(",")}}`;
27
+ }
28
+ return `**/*.${extensions[0]}`;
21
29
  }
22
30
  /**
23
31
  * Utility function to clean file extensions
@@ -63,12 +63,23 @@ function runCodeshift(input, options, program) {
63
63
  return __awaiter(this, void 0, void 0, function* () {
64
64
  var _a;
65
65
  const codemodPath = node_path_1.default.join(__dirname, `./transforms/${(0, migrations_1.getMigrationPath)(input)}.js`);
66
- const filepaths = fast_glob_1.default.sync([(_a = options.glob) !== null && _a !== void 0 ? _a : (0, codeshift_utils_1.getDefaultGlob)(options === null || options === void 0 ? void 0 : options.ext)], {
66
+ console.info(chalk_1.default.greenBright.bold("\nWelcome to Aksel codemods!"));
67
+ console.info("\nRunning migration:", chalk_1.default.green(input));
68
+ const globList = (_a = options.glob) !== null && _a !== void 0 ? _a : (0, codeshift_utils_1.getDefaultGlob)(options === null || options === void 0 ? void 0 : options.ext);
69
+ console.info(chalk_1.default.gray(`Using glob pattern(s): ${globList}\nWorking directory: ${process.cwd()}\n`));
70
+ const filepaths = fast_glob_1.default.sync(globList, {
67
71
  cwd: process.cwd(),
68
72
  ignore: codeshift_utils_1.GLOB_IGNORE_PATTERNS,
73
+ /**
74
+ * When globbing, do not follow symlinks to avoid processing files outside the directory.
75
+ * This is most likely to happen in monorepos where node_modules may contain symlinks to packages
76
+ * in other parts of the repo.
77
+ *
78
+ * While node_modules is already ignored via GLOB_IGNORE_PATTERNS, if user globs upwards (e.g., using '../src/**'),
79
+ * that ignore-pattern may be ignored, leading to unintended file processing.
80
+ */
81
+ followSymbolicLinks: false,
69
82
  });
70
- console.info("\nRunning migration:", chalk_1.default.green("input"));
71
- (options === null || options === void 0 ? void 0 : options.glob) && console.info(`Using glob: ${chalk_1.default.green(options.glob)}\n`);
72
83
  const warning = (0, migrations_1.getWarning)(input);
73
84
  const unsafeExtensions = (0, migrations_1.getIgnoredFileExtensions)(input);
74
85
  let safeFilepaths = filepaths;
@@ -33,15 +33,22 @@ function transformer(file, api) {
33
33
  },
34
34
  },
35
35
  });
36
+ const predefinedReplacents = predefinedReplacentset();
36
37
  const tokenComments = [];
37
38
  for (const astElement of astElements.paths()) {
38
39
  for (const prop of propsAffected) {
39
40
  (0, ast_1.findProps)({ j, path: astElement, name: prop }).forEach((attr) => {
40
41
  const attrvalue = attr.value.value;
41
42
  if (attrvalue.type === "StringLiteral") {
43
+ /**
44
+ * Skips if the replacement token already set
45
+ */
46
+ if (predefinedReplacents.has(addPrefix(attrvalue.value, prop))) {
47
+ return;
48
+ }
42
49
  const config = legacy_tokens_1.legacyTokenConfig[attrvalue.value];
43
50
  if (config === null || config === void 0 ? void 0 : config.replacement) {
44
- attrvalue.value = config.replacement;
51
+ attrvalue.value = cleanReplacementToken(config.replacement, prop);
45
52
  }
46
53
  else {
47
54
  const tokenComment = {
@@ -57,9 +64,15 @@ function transformer(file, api) {
57
64
  else if (attrvalue.type === "JSXExpressionContainer" &&
58
65
  attrvalue.expression.type === "StringLiteral") {
59
66
  const literal = attrvalue.expression;
67
+ /**
68
+ * Skips if the replacement token already set
69
+ */
70
+ if (predefinedReplacents.has(addPrefix(literal.value, prop))) {
71
+ return;
72
+ }
60
73
  const config = legacy_tokens_1.legacyTokenConfig[literal.value];
61
74
  if (config === null || config === void 0 ? void 0 : config.replacement) {
62
- literal.value = config.replacement;
75
+ literal.value = cleanReplacementToken(config.replacement, prop);
63
76
  }
64
77
  else {
65
78
  const tokenComment = {
@@ -149,3 +162,38 @@ function convertBorderRadiusToRadius(oldValue) {
149
162
  }
150
163
  return newRadius.join(" ");
151
164
  }
165
+ /**
166
+ * New props in Box do not have bg- or border- prefixes
167
+ * This function removes those prefixes from the tokens
168
+ */
169
+ function cleanReplacementToken(token, type) {
170
+ if (type === "background") {
171
+ return token.replace("bg-", "");
172
+ }
173
+ if (type === "borderColor") {
174
+ return token.replace("border-", "");
175
+ }
176
+ return token;
177
+ }
178
+ /**
179
+ * Adds bg- or border- prefixes to tokens for comparison with existing replacements
180
+ */
181
+ function addPrefix(token, type) {
182
+ if (type === "background") {
183
+ return `bg-${token}`;
184
+ }
185
+ if (type === "borderColor") {
186
+ return `border-${token}`;
187
+ }
188
+ return token;
189
+ }
190
+ function predefinedReplacentset() {
191
+ const set = new Set();
192
+ for (const key in legacy_tokens_1.legacyTokenConfig) {
193
+ const config = legacy_tokens_1.legacyTokenConfig[key];
194
+ if (config.replacement) {
195
+ set.add(config.replacement);
196
+ }
197
+ }
198
+ return set;
199
+ }
@@ -64,7 +64,7 @@ function findJSXElement(input) {
64
64
  */
65
65
  function findProps(input) {
66
66
  const { j, path, name } = input;
67
- return j(path).find(j.JSXAttribute, {
67
+ return j(path.get("openingElement")).find(j.JSXAttribute, {
68
68
  name: {
69
69
  name,
70
70
  },
@@ -6,23 +6,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.darksideCommand = darksideCommand;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const commander_1 = require("commander");
9
- const validation_js_1 = require("../codemod/validation.js");
10
- // import figlet from "figlet";
11
- // import { getMigrationString } from "./migrations.js";
12
9
  const run_tooling_js_1 = require("./run-tooling.js");
13
10
  const program = new commander_1.Command();
14
11
  function darksideCommand() {
15
- program.name(`${chalk_1.default.blueBright(`npx @navikt/aksel`)}`);
12
+ program.name(`${chalk_1.default.blueBright(`npx @navikt/aksel v8-tokens`)}`);
16
13
  program
17
14
  .option("-g, --glob [glob]", "Globbing pattern, overrides --ext! Run with 'noglob' if using zsh-terminal. ")
15
+ .option("-e, --ext [ext]", "File extensions to include, defaults to 'js,ts,jsx,tsx,css,scss,less'")
18
16
  .option("-d, --dry-run", "Dry run, no changes will be made")
19
17
  .option("-f, --force", "Forcibly run updates without checking git-changes")
20
- .description("Update tool for darkside token updates");
18
+ .description("Update tool for v8 token updates");
21
19
  program.parse();
22
20
  const options = program.opts();
23
- /* Makes sure that you don't migrate lots of files while having other uncommitted changes */
24
- if (!options.force) {
25
- (0, validation_js_1.validateGit)(options, program);
26
- }
27
21
  (0, run_tooling_js_1.runTooling)(options, program);
28
22
  }
@@ -63,50 +63,46 @@ const TRANSFORMS = {
63
63
  "js-tokens": "./transforms/darkside-tokens-js",
64
64
  "tailwind-tokens": "./transforms/darkside-tokens-tailwind",
65
65
  };
66
- const TASK_MENU = {
67
- type: "select",
68
- name: "task",
69
- message: "Task",
70
- initial: "status",
71
- choices: [
72
- { message: "Check status", name: "status" },
73
- { message: "Print remaining tokens", name: "print-remaining-tokens" },
74
- { message: "Migrate CSS tokens", name: "css-tokens" },
75
- { message: "Migrate Scss tokens", name: "scss-tokens" },
76
- { message: "Migrate Less tokens", name: "less-tokens" },
77
- { message: "Migrate JS tokens", name: "js-tokens" },
78
- { message: "Migrate tailwind tokens", name: "tailwind-tokens" },
79
- { message: "Run all migrations", name: "run-all-migrations" },
80
- { message: "Exit", name: "exit" },
81
- ],
82
- };
83
66
  /**
84
67
  * Main entry point for the tooling system
85
68
  */
86
69
  function runTooling(options, program) {
87
70
  return __awaiter(this, void 0, void 0, function* () {
88
71
  var _a;
72
+ console.info(chalk_1.default.greenBright.bold("\nWelcome to the Aksel v8 token migration tool!"));
73
+ const globList = (_a = options.glob) !== null && _a !== void 0 ? _a : (0, codeshift_utils_1.getDefaultGlob)(options === null || options === void 0 ? void 0 : options.ext);
74
+ console.info(chalk_1.default.gray(`Using glob pattern(s): ${globList}\nWorking directory: ${process.cwd()}\n`));
89
75
  // Find matching files based on glob pattern
90
- const filepaths = fast_glob_1.default.sync([(_a = options.glob) !== null && _a !== void 0 ? _a : (0, codeshift_utils_1.getDefaultGlob)(options === null || options === void 0 ? void 0 : options.ext)], {
76
+ const filepaths = yield (0, fast_glob_1.default)(globList, {
91
77
  cwd: process.cwd(),
92
78
  ignore: codeshift_utils_1.GLOB_IGNORE_PATTERNS,
79
+ /**
80
+ * When globbing, do not follow symlinks to avoid processing files outside the directory.
81
+ * This is most likely to happen in monorepos where node_modules may contain symlinks to packages
82
+ * in other parts of the repo.
83
+ *
84
+ * While node_modules is already ignored via GLOB_IGNORE_PATTERNS, if user globs upwards (e.g., using '../src/**'),
85
+ * that ignore-pattern may be ignored, leading to unintended file processing.
86
+ */
87
+ followSymbolicLinks: false,
93
88
  });
94
89
  if (options.dryRun) {
95
90
  console.info(chalk_1.default.yellow("Running in dry-run mode, no changes will be made"));
96
91
  }
97
92
  // Show initial status
98
- (0, status_1.getStatus)(filepaths);
93
+ const initialStatus = (0, status_1.getStatus)(filepaths);
99
94
  // Task execution loop
100
- let task = yield getNextTask();
95
+ let task = yield getNextTask(initialStatus.status);
96
+ let currentStatus = initialStatus;
101
97
  while (task !== "exit") {
102
98
  console.info("\n\n");
103
99
  try {
104
- yield executeTask(task, filepaths, options, program);
100
+ currentStatus = yield executeTask(task, filepaths, options, program, currentStatus, () => (0, status_1.getStatus)(filepaths, "no-print"));
105
101
  }
106
102
  catch (error) {
107
103
  program.error(chalk_1.default.red("Error:", error.message));
108
104
  }
109
- task = yield getNextTask();
105
+ task = yield getNextTask(currentStatus.status);
110
106
  }
111
107
  process.exit(0);
112
108
  });
@@ -114,27 +110,34 @@ function runTooling(options, program) {
114
110
  /**
115
111
  * Executes the selected task
116
112
  */
117
- function executeTask(task, filepaths, options, program) {
113
+ function executeTask(task, filepaths, options, program, statusStore, updateStatus) {
118
114
  return __awaiter(this, void 0, void 0, function* () {
119
115
  switch (task) {
120
116
  case "status":
121
- (0, status_1.getStatus)(filepaths);
122
- break;
123
- case "print-remaining-tokens":
124
- (0, print_remaining_1.printRemaining)(filepaths);
125
- break;
117
+ return updateStatus();
118
+ case "print-remaining-tokens": {
119
+ const newStatus = updateStatus();
120
+ yield (0, print_remaining_1.printRemaining)(filepaths, newStatus.status);
121
+ return newStatus;
122
+ }
126
123
  case "css-tokens":
127
124
  case "scss-tokens":
128
125
  case "less-tokens":
129
126
  case "js-tokens":
130
127
  case "tailwind-tokens": {
131
- const updatedStatus = (0, status_1.getStatus)(filepaths, "no-print").status;
132
- const scopedFiles = getScopedFilesForTask(task, filepaths, updatedStatus);
133
- yield runCodeshift(task, scopedFiles, {
128
+ if (!options.force) {
129
+ (0, validation_1.validateGit)(options, program);
130
+ }
131
+ const scopedFiles = getScopedFilesForTask(task, filepaths, statusStore.status);
132
+ const tokensBefore = getTokenCount(statusStore.status, task);
133
+ const stats = yield runCodeshift(task, scopedFiles, {
134
134
  dryRun: options.dryRun,
135
135
  force: options.force,
136
136
  });
137
- break;
137
+ const newStatus = updateStatus();
138
+ const tokensAfter = getTokenCount(newStatus.status, task);
139
+ printSummary(task, stats, tokensBefore, tokensAfter);
140
+ return newStatus;
138
141
  }
139
142
  case "run-all-migrations": {
140
143
  const tasks = [
@@ -144,20 +147,45 @@ function executeTask(task, filepaths, options, program) {
144
147
  "js-tokens",
145
148
  "tailwind-tokens",
146
149
  ];
150
+ if (!options.force) {
151
+ (0, validation_1.validateGit)(options, program);
152
+ }
153
+ let currentStatus = statusStore;
154
+ const summaryData = [];
147
155
  for (const migrationTask of tasks) {
148
- if (!options.force) {
149
- (0, validation_1.validateGit)(options, program);
150
- }
151
156
  console.info(`\nRunning ${migrationTask}...`);
152
- yield runCodeshift(migrationTask, filepaths, {
157
+ const scopedFiles = getScopedFilesForTask(migrationTask, filepaths, currentStatus.status);
158
+ const tokensBefore = getTokenCount(currentStatus.status, migrationTask);
159
+ const stats = yield runCodeshift(migrationTask, scopedFiles, {
153
160
  dryRun: options.dryRun,
154
161
  force: true,
155
162
  });
163
+ currentStatus = updateStatus();
164
+ const tokensAfter = getTokenCount(currentStatus.status, migrationTask);
165
+ summaryData.push({
166
+ task: migrationTask,
167
+ stats,
168
+ tokensBefore,
169
+ tokensAfter,
170
+ });
171
+ }
172
+ console.info(chalk_1.default.bold(`\nMigration Summary:`));
173
+ console.info("-".repeat(60));
174
+ for (const data of summaryData) {
175
+ const replaced = data.tokensBefore - data.tokensAfter;
176
+ const remaining = data.tokensAfter;
177
+ const icon = remaining === 0 ? "✨" : "⚠️";
178
+ console.info(`${chalk_1.default.bold(data.task)}:`);
179
+ console.info(` Files changed: ${data.stats.ok}`);
180
+ console.info(` Tokens replaced: ${replaced}`);
181
+ console.info(` ${icon} Remaining: ${remaining}`);
182
+ console.info("");
156
183
  }
157
- break;
184
+ return currentStatus;
158
185
  }
159
186
  default:
160
187
  program.error(chalk_1.default.red(`Unknown task: ${task}`));
188
+ return statusStore;
161
189
  }
162
190
  });
163
191
  }
@@ -201,7 +229,7 @@ function runCodeshift(task, filepaths, options) {
201
229
  throw new Error(`No transform found for task: ${task}`);
202
230
  }
203
231
  const codemodPath = node_path_1.default.join(__dirname, `${TRANSFORMS[task]}.js`);
204
- yield jscodeshift.run(codemodPath, filepaths, {
232
+ return yield jscodeshift.run(codemodPath, filepaths, {
205
233
  babel: true,
206
234
  ignorePattern: codeshift_utils_1.GLOB_IGNORE_PATTERNS,
207
235
  parser: "tsx",
@@ -218,12 +246,52 @@ function runCodeshift(task, filepaths, options) {
218
246
  /**
219
247
  * Prompts the user for the next task to run
220
248
  */
221
- function getNextTask() {
249
+ function getNextTask(status) {
222
250
  return __awaiter(this, void 0, void 0, function* () {
251
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
252
+ const getMessage = (base, tokens) => {
253
+ if (!status)
254
+ return base;
255
+ const fileCount = new Set(tokens.map((t) => t.fileName)).size;
256
+ if (fileCount === 0)
257
+ return `${base} (Done)`;
258
+ return `${base} (${fileCount} files)`;
259
+ };
260
+ const choices = [
261
+ { message: "Check status", name: "status" },
262
+ { message: "Print status", name: "print-remaining-tokens" },
263
+ {
264
+ message: getMessage("Migrate CSS tokens", (_b = (_a = status === null || status === void 0 ? void 0 : status.css) === null || _a === void 0 ? void 0 : _a.legacy) !== null && _b !== void 0 ? _b : []),
265
+ name: "css-tokens",
266
+ },
267
+ {
268
+ message: getMessage("Migrate Scss tokens", (_d = (_c = status === null || status === void 0 ? void 0 : status.scss) === null || _c === void 0 ? void 0 : _c.legacy) !== null && _d !== void 0 ? _d : []),
269
+ name: "scss-tokens",
270
+ },
271
+ {
272
+ message: getMessage("Migrate Less tokens", (_f = (_e = status === null || status === void 0 ? void 0 : status.less) === null || _e === void 0 ? void 0 : _e.legacy) !== null && _f !== void 0 ? _f : []),
273
+ name: "less-tokens",
274
+ },
275
+ {
276
+ message: getMessage("Migrate JS tokens", (_h = (_g = status === null || status === void 0 ? void 0 : status.js) === null || _g === void 0 ? void 0 : _g.legacy) !== null && _h !== void 0 ? _h : []),
277
+ name: "js-tokens",
278
+ },
279
+ {
280
+ message: getMessage("Migrate tailwind tokens", (_k = (_j = status === null || status === void 0 ? void 0 : status.tailwind) === null || _j === void 0 ? void 0 : _j.legacy) !== null && _k !== void 0 ? _k : []),
281
+ name: "tailwind-tokens",
282
+ },
283
+ { message: "Run all migrations", name: "run-all-migrations" },
284
+ { message: "Exit", name: "exit" },
285
+ ];
223
286
  try {
224
- const response = yield enquirer_1.default.prompt([
225
- Object.assign(Object.assign({}, TASK_MENU), { onCancel: () => process.exit(1) }),
226
- ]);
287
+ const response = yield enquirer_1.default.prompt({
288
+ type: "select",
289
+ name: "task",
290
+ message: "Task",
291
+ initial: "status",
292
+ choices,
293
+ onCancel: () => process.exit(1),
294
+ });
227
295
  return response.task;
228
296
  }
229
297
  catch (error) {
@@ -238,3 +306,31 @@ function getNextTask() {
238
306
  }
239
307
  });
240
308
  }
309
+ function getTokenCount(status, task) {
310
+ switch (task) {
311
+ case "css-tokens":
312
+ return status.css.legacy.length;
313
+ case "scss-tokens":
314
+ return status.scss.legacy.length;
315
+ case "less-tokens":
316
+ return status.less.legacy.length;
317
+ case "js-tokens":
318
+ return status.js.legacy.length;
319
+ case "tailwind-tokens":
320
+ return status.tailwind.legacy.length;
321
+ default:
322
+ return 0;
323
+ }
324
+ }
325
+ function printSummary(task, stats, tokensBefore, tokensAfter) {
326
+ console.info(chalk_1.default.bold(`\nMigration Summary for ${task}:`));
327
+ console.info("-".repeat(40));
328
+ console.info(`✅ Files changed: ${stats.ok}`);
329
+ console.info(`✅ Tokens replaced: ${tokensBefore - tokensAfter}`);
330
+ if (tokensAfter > 0) {
331
+ console.info(chalk_1.default.yellow(`⚠️ Tokens remaining: ${tokensAfter} (manual intervention needed)`));
332
+ }
333
+ else {
334
+ console.info(chalk_1.default.green(`✨ Tokens remaining: ${tokensAfter}`));
335
+ }
336
+ }
@@ -1,51 +1,156 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
5
14
  Object.defineProperty(exports, "__esModule", { value: true });
6
15
  exports.printRemaining = printRemaining;
16
+ const clipboardy_1 = __importDefault(require("clipboardy"));
17
+ const enquirer_1 = __importDefault(require("enquirer"));
7
18
  const node_path_1 = __importDefault(require("node:path"));
8
19
  const status_1 = require("./status");
9
- function printRemaining(files) {
10
- process.stdout.write("\nAnalyzing...");
11
- const statusObj = (0, status_1.getStatus)(files, "no-print").status;
12
- Object.entries(statusObj).forEach(([tokenType, data]) => {
13
- console.group(`\n${tokenType.toUpperCase()}:`);
14
- const fileLinks = [];
15
- data.legacy.forEach((tokenData) => {
16
- fileLinks.push(`${tokenData.name.replace(":", "")}:${tokenData.fileName}:${tokenData.lineNumber}:${tokenData.columnNumber}`);
17
- });
18
- if (fileLinks.length === 0) {
19
- console.info("Nothing to update.");
20
- console.groupEnd();
20
+ function printRemaining(files, status) {
21
+ return __awaiter(this, void 0, void 0, function* () {
22
+ process.stdout.write("\nAnalyzing...");
23
+ /**
24
+ * Skip re-calculating status if already provided
25
+ */
26
+ const statusObj = status !== null && status !== void 0 ? status : (0, status_1.getStatus)(files, "no-print").status;
27
+ /* Flatten all legacy tokens */
28
+ const allTokens = Object.values(statusObj).flatMap((data) => data.legacy);
29
+ if (allTokens.length === 0) {
30
+ console.info("\nNo legacy tokens found!");
31
+ return;
21
32
  }
22
- // Ensure every string is unique
23
- const uniqueFileLinks = Array.from(new Set(fileLinks));
24
- // Sort the unique fileLinks based on fileName first, lineNumber second, and columnNumber third
25
- uniqueFileLinks.sort((a, b) => {
26
- const [fileA, lineA, columnA] = a.split(":");
27
- const [fileB, lineB, columnB] = b.split(":");
28
- if (fileA !== fileB) {
29
- return fileA.localeCompare(fileB);
30
- }
31
- if (Number(lineA) !== Number(lineB)) {
32
- return Number(lineA) - Number(lineB);
33
- }
34
- return Number(columnA) - Number(columnB);
35
- });
36
- // Print the unique and sorted fileLinks as clickable links with full path
37
- uniqueFileLinks.forEach((link) => {
38
- const [tokenName, fileName, lineNumber, columnNumber] = link.split(":");
39
- const fullPath = node_path_1.default.resolve(process.cwd(), fileName);
40
- const withComment = data.legacy.find((token) => {
41
- return token.name === tokenName && token.comment;
33
+ const response = yield enquirer_1.default.prompt([
34
+ {
35
+ type: "select",
36
+ name: "groupBy",
37
+ message: "How would you like to group the remaining tokens?",
38
+ choices: [
39
+ { message: "By File", name: "file" },
40
+ { message: "By Token", name: "token" },
41
+ ],
42
+ },
43
+ {
44
+ type: "confirm",
45
+ name: "copy",
46
+ message: "Copy report to clipboard?",
47
+ initial: true,
48
+ },
49
+ ]);
50
+ const { groupBy, copy } = response;
51
+ console.info("\n");
52
+ const log = (str, indent = 0) => {
53
+ const prefix = " ".repeat(indent);
54
+ console.info(prefix + str);
55
+ };
56
+ let jsonOutput;
57
+ /**
58
+ * Group by filename
59
+ */
60
+ if (groupBy === "file") {
61
+ const byFile = new Map();
62
+ allTokens.forEach((token) => {
63
+ if (!byFile.has(token.fileName)) {
64
+ byFile.set(token.fileName, []);
65
+ }
66
+ byFile.get(token.fileName).push(token);
67
+ });
68
+ /* Sort files by number of tokens (descending) */
69
+ const sortedFiles = Array.from(byFile.entries()).sort((a, b) => b[1].length - a[1].length);
70
+ const fileOutput = [];
71
+ sortedFiles.forEach(([fileName, tokens]) => {
72
+ const fullPath = node_path_1.default.resolve(process.cwd(), fileName);
73
+ log(`${fileName} (${tokens.length} tokens)`);
74
+ /* Sort tokens in file by line number */
75
+ tokens.sort((a, b) => a.lineNumber - b.lineNumber);
76
+ const fileEntry = {
77
+ file: fileName,
78
+ fullPath,
79
+ count: tokens.length,
80
+ tokens: [],
81
+ };
82
+ tokens.forEach((token) => {
83
+ if (token.comment) {
84
+ log(`/* ${token.comment} */`, 1);
85
+ }
86
+ log(`${token.name}: ${fullPath}:${token.lineNumber}:${token.columnNumber}`, 1);
87
+ fileEntry.tokens.push({
88
+ name: token.name,
89
+ line: token.lineNumber,
90
+ column: token.columnNumber,
91
+ comment: token.comment,
92
+ link: `file://${fullPath}`,
93
+ });
94
+ });
95
+ /* Empty line */
96
+ log("");
97
+ fileOutput.push(fileEntry);
42
98
  });
43
- if (withComment) {
44
- console.info(`\n/* ${withComment.comment} */`);
99
+ jsonOutput = fileOutput;
100
+ }
101
+ else {
102
+ /* Group by token name */
103
+ const byToken = new Map();
104
+ allTokens.forEach((token) => {
105
+ if (!byToken.has(token.name)) {
106
+ byToken.set(token.name, []);
107
+ }
108
+ byToken.get(token.name).push(token);
109
+ });
110
+ /* Sort tokens by frequency (descending) */
111
+ const sortedTokens = Array.from(byToken.entries()).sort((a, b) => b[1].length - a[1].length);
112
+ const tokenOutput = [];
113
+ sortedTokens.forEach(([tokenName, tokens]) => {
114
+ var _a;
115
+ log(`${tokenName} (${tokens.length} occurrences)`);
116
+ /**
117
+ * We can assume all comments are the same for a "tokenName"
118
+ */
119
+ const foundComment = (_a = tokens.find((t) => t.comment)) === null || _a === void 0 ? void 0 : _a.comment;
120
+ if (foundComment) {
121
+ log(`/* ${foundComment} */`, 1);
122
+ }
123
+ const tokenEntry = {
124
+ token: tokenName,
125
+ count: tokens.length,
126
+ occurrences: [],
127
+ };
128
+ tokens.forEach((token) => {
129
+ const fullPath = node_path_1.default.resolve(process.cwd(), token.fileName);
130
+ log(`${fullPath}:${token.lineNumber}:${token.columnNumber}`, 1);
131
+ tokenEntry.occurrences.push({
132
+ file: token.fileName,
133
+ fullPath,
134
+ line: token.lineNumber,
135
+ column: token.columnNumber,
136
+ comment: token.comment,
137
+ link: `file://${fullPath}`,
138
+ });
139
+ });
140
+ /* Empty line */
141
+ log("");
142
+ tokenOutput.push(tokenEntry);
143
+ });
144
+ jsonOutput = tokenOutput;
145
+ }
146
+ if (copy) {
147
+ try {
148
+ clipboardy_1.default.writeSync(JSON.stringify(jsonOutput, null, 2));
149
+ console.info("✅ Report (JSON) copied to clipboard!");
45
150
  }
46
- console.info(`${tokenName}: file://${fullPath}:${lineNumber}:${columnNumber}`);
47
- });
48
- console.groupEnd();
151
+ catch (error) {
152
+ console.error("❌ Failed to copy to clipboard:", error.message);
153
+ }
154
+ }
49
155
  });
50
- console.info("\n");
51
156
  }
@@ -4,13 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getStatus = getStatus;
7
+ exports.getCharacterPositionInFile = getCharacterPositionInFile;
8
+ exports.getLineStarts = getLineStarts;
7
9
  const cli_progress_1 = __importDefault(require("cli-progress"));
8
10
  const node_fs_1 = __importDefault(require("node:fs"));
11
+ const translate_token_1 = require("../../codemod/utils/translate-token");
9
12
  const TokenStatus_1 = require("../config/TokenStatus");
10
13
  const darkside_tokens_1 = require("../config/darkside.tokens");
11
14
  const legacy_component_tokens_1 = require("../config/legacy-component.tokens");
12
15
  const legacy_tokens_1 = require("../config/legacy.tokens");
13
- const token_regex_1 = require("../config/token-regex");
14
16
  const StatusStore = new TokenStatus_1.TokenStatus();
15
17
  /**
16
18
  * Get the status of the tokens in the files
@@ -25,13 +27,57 @@ function getStatus(files, action = "print") {
25
27
  progressBar.start(files.length, 0);
26
28
  }
27
29
  StatusStore.initStatus();
30
+ /**
31
+ * Prepare search terms for legacy and darkside tokens.
32
+ * By pre-computing these sets, we save re-calculating them for each file,
33
+ * improving performance when processing large numbers of files.
34
+ */
35
+ const legacySearchTerms = getLegacySearchTerms();
36
+ const darksideSearchTerms = getDarksideSearchTerms();
37
+ const legacyComponentTokensSet = new Set(legacy_component_tokens_1.legacyComponentTokenList);
38
+ /**
39
+ * Pre-computed regex for legacy component tokens
40
+ */
41
+ const legacyRegex = new RegExp(`(${legacy_component_tokens_1.legacyComponentTokenList.map((t) => `${t}:`).join("|")})`, "gm");
42
+ /**
43
+ * Process each file to find and record token usages
44
+ */
28
45
  files.forEach((fileName, index) => {
29
46
  const fileSrc = node_fs_1.default.readFileSync(fileName, "utf8");
47
+ /**
48
+ * Create a set of all words in the file to quickly check for potential matches
49
+ */
50
+ const fileWords = new Set(fileSrc.match(/[a-zA-Z0-9_@$-]+/g) || []);
51
+ let lineStarts;
52
+ /**
53
+ * Gets line-start positions for the file, caching the result.
54
+ * We only calculate this if we actually find a token match, saving processing time.
55
+ */
56
+ const getLineStartsLazy = () => {
57
+ if (!lineStarts) {
58
+ lineStarts = getLineStarts(fileSrc);
59
+ }
60
+ return lineStarts;
61
+ };
30
62
  /**
31
63
  * We first parse trough all legacy tokens (--a-) prefixed tokens
32
64
  */
33
65
  for (const [legacyToken, config] of Object.entries(legacy_tokens_1.legacyTokenConfig)) {
34
- if (!(0, token_regex_1.getTokenRegex)(legacyToken, "css").test(fileSrc)) {
66
+ const terms = legacySearchTerms.get(legacyToken);
67
+ /**
68
+ * Optimization: Check if any of the search terms exist in the file words set
69
+ * before running expensive regex operations.
70
+ */
71
+ let found = false;
72
+ if (terms) {
73
+ for (const term of terms) {
74
+ if (fileWords.has(term)) {
75
+ found = true;
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ if (!found) {
35
81
  continue;
36
82
  }
37
83
  for (const [regexKey, regex] of Object.entries(config.regexes)) {
@@ -40,7 +86,7 @@ function getStatus(files, action = "print") {
40
86
  }
41
87
  let match = regex.exec(fileSrc);
42
88
  while (match) {
43
- const { row, column } = getWordPositionInFile(fileSrc, match.index);
89
+ const { row, column } = getCharacterPositionInFile(match.index, getLineStartsLazy());
44
90
  StatusStore.add({
45
91
  isLegacy: true,
46
92
  comment: config.comment,
@@ -57,23 +103,43 @@ function getStatus(files, action = "print") {
57
103
  }
58
104
  }
59
105
  }
60
- const legacyRegex = new RegExp(`(${legacy_component_tokens_1.legacyComponentTokenList.map((t) => `${t}:`).join("|")})`, "gm");
61
- let legacyMatch = legacyRegex.exec(fileSrc);
62
- while (legacyMatch !== null) {
63
- const { row, column } = getWordPositionInFile(fileSrc, legacyMatch.index);
64
- StatusStore.add({
65
- isLegacy: true,
66
- type: "component",
67
- columnNumber: column,
68
- lineNumber: row,
69
- canAutoMigrate: false,
70
- fileName,
71
- name: legacyMatch[0],
72
- });
73
- legacyMatch = legacyRegex.exec(fileSrc);
106
+ let hasLegacyComponentToken = false;
107
+ for (const token of legacyComponentTokensSet) {
108
+ if (fileWords.has(token)) {
109
+ hasLegacyComponentToken = true;
110
+ break;
111
+ }
112
+ }
113
+ if (hasLegacyComponentToken) {
114
+ legacyRegex.lastIndex = 0;
115
+ let legacyMatch = legacyRegex.exec(fileSrc);
116
+ while (legacyMatch !== null) {
117
+ const { row, column } = getCharacterPositionInFile(legacyMatch.index, getLineStartsLazy());
118
+ StatusStore.add({
119
+ isLegacy: true,
120
+ type: "component",
121
+ columnNumber: column,
122
+ lineNumber: row,
123
+ canAutoMigrate: false,
124
+ fileName,
125
+ name: legacyMatch[0],
126
+ });
127
+ legacyMatch = legacyRegex.exec(fileSrc);
128
+ }
74
129
  }
75
130
  for (const [newTokenName, config] of Object.entries(darkside_tokens_1.darksideTokenConfig)) {
76
- if (!(0, token_regex_1.getTokenRegex)(newTokenName, "css").test(fileSrc)) {
131
+ const terms = darksideSearchTerms.get(newTokenName);
132
+ /* Optimization: Check if any of the search terms exist in the file words set */
133
+ let found = false;
134
+ if (terms) {
135
+ for (const term of terms) {
136
+ if (fileWords.has(term)) {
137
+ found = true;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ if (!found) {
77
143
  continue;
78
144
  }
79
145
  for (const [regexKey, regex] of Object.entries(config.regexes)) {
@@ -82,7 +148,7 @@ function getStatus(files, action = "print") {
82
148
  }
83
149
  let match = regex.exec(fileSrc);
84
150
  while (match) {
85
- const { row, column } = getWordPositionInFile(fileSrc, match.index);
151
+ const { row, column } = getCharacterPositionInFile(match.index, getLineStartsLazy());
86
152
  StatusStore.add({
87
153
  isLegacy: false,
88
154
  type: regexKey,
@@ -109,17 +175,68 @@ function getStatus(files, action = "print") {
109
175
  console.info("\n");
110
176
  return StatusStore;
111
177
  }
112
- function getWordPositionInFile(fileContent, index) {
113
- const lines = fileContent.split("\n");
114
- let lineNumber = 1;
115
- let charCount = 0;
116
- for (let i = 0; i < lines.length; i++) {
117
- const lineLength = lines[i].length + 1; // +1 to account for the newline character that was removed by split
118
- if (charCount + lineLength > index) {
119
- return { row: lineNumber, column: index - charCount + 1 };
178
+ function getLegacySearchTerms() {
179
+ const legacySearchTerms = new Map();
180
+ for (const [legacyToken, config] of Object.entries(legacy_tokens_1.legacyTokenConfig)) {
181
+ const terms = new Set();
182
+ const tokenName = `--a-${legacyToken}`;
183
+ terms.add(tokenName);
184
+ terms.add((0, translate_token_1.translateToken)(tokenName, "scss"));
185
+ terms.add((0, translate_token_1.translateToken)(tokenName, "less"));
186
+ terms.add((0, translate_token_1.translateToken)(tokenName, "js"));
187
+ if (config.twOld) {
188
+ config.twOld.split(",").forEach((t) => terms.add(t.trim()));
189
+ }
190
+ legacySearchTerms.set(legacyToken, terms);
191
+ }
192
+ return legacySearchTerms;
193
+ }
194
+ function getDarksideSearchTerms() {
195
+ const darksideSearchTerms = new Map();
196
+ for (const [newTokenName, config] of Object.entries(darkside_tokens_1.darksideTokenConfig)) {
197
+ const terms = new Set();
198
+ const tokenName = `--ax-${newTokenName}`;
199
+ terms.add(tokenName);
200
+ terms.add((0, translate_token_1.translateToken)(tokenName, "scss"));
201
+ terms.add((0, translate_token_1.translateToken)(tokenName, "less"));
202
+ terms.add((0, translate_token_1.translateToken)(newTokenName, "js"));
203
+ terms.add(newTokenName);
204
+ if (config.tw) {
205
+ config.tw.split(",").forEach((t) => terms.add(t.trim()));
206
+ }
207
+ darksideSearchTerms.set(newTokenName, terms);
208
+ }
209
+ return darksideSearchTerms;
210
+ }
211
+ /**
212
+ * Given the content of a file, returns an array of line start positions.
213
+ */
214
+ function getLineStarts(content) {
215
+ const starts = [0];
216
+ let lineIndex = content.indexOf("\n", 0);
217
+ while (lineIndex !== -1) {
218
+ starts.push(lineIndex + 1);
219
+ lineIndex = content.indexOf("\n", lineIndex + 1);
220
+ }
221
+ return starts;
222
+ }
223
+ /**
224
+ * Given a character index and an array of line start positions,
225
+ * returns the corresponding row and column numbers.
226
+ */
227
+ function getCharacterPositionInFile(index, lineStarts) {
228
+ let low = 0;
229
+ let high = lineStarts.length - 1;
230
+ let lineIndex = 0;
231
+ while (low <= high) {
232
+ const mid = (low + high) >>> 1;
233
+ if (lineStarts[mid] <= index) {
234
+ lineIndex = mid;
235
+ low = mid + 1;
236
+ }
237
+ else {
238
+ high = mid - 1;
120
239
  }
121
- charCount += lineLength;
122
- lineNumber++;
123
240
  }
124
- return { row: lineNumber, column: 0 }; // Should not reach here if the index is within the file content range
241
+ return { row: lineIndex + 1, column: index - lineStarts[lineIndex] + 1 };
125
242
  }
@@ -4,14 +4,29 @@ exports.default = transformer;
4
4
  const legacy_tokens_1 = require("../config/legacy.tokens");
5
5
  function transformer(file) {
6
6
  let src = file.source;
7
- for (const [oldToken, config] of Object.entries(legacy_tokens_1.legacyTokenConfig)) {
8
- const oldCSSVar = `--a-${oldToken}`;
9
- /* We update all re-definitions of a token to a "legacy" version */
10
- const replaceRegex = new RegExp("(" + `${oldCSSVar}:` + ")", "gm");
11
- src = src.replace(replaceRegex, `--aksel-legacy${oldCSSVar.replace("--", "__")}:`);
12
- if (config.replacement.length > 0) {
13
- src = src.replace(config.regexes.css, `--ax-${config.replacement}`);
7
+ /*
8
+ 1. Replace definitions: --a-token: -> --aksel-legacy__a-token:
9
+ Matches "--a-token" followed by optional whitespace and a colon.
10
+ Uses negative lookbehind to ensure we don't match "--not-a-token".
11
+ */
12
+ src = src.replace(/(?<![\w-])(--a-[\w-]+)(\s*:)/g, (match, tokenName, suffix) => {
13
+ const key = tokenName.replace("--a-", "");
14
+ if (legacy_tokens_1.legacyTokenConfig[key]) {
15
+ return `--aksel-legacy${tokenName.replace("--", "__")}${suffix}`;
14
16
  }
15
- }
17
+ return match;
18
+ });
19
+ /*
20
+ 2. Replace usages: --a-token -> --ax-replacement
21
+ Matches "--a-token" with word boundaries.
22
+ */
23
+ src = src.replace(/(?<![\w-])(--a-[\w-]+)(?![\w-])/g, (match, tokenName) => {
24
+ const key = tokenName.replace("--a-", "");
25
+ const config = legacy_tokens_1.legacyTokenConfig[key];
26
+ if (config === null || config === void 0 ? void 0 : config.replacement) {
27
+ return `--ax-${config.replacement}`;
28
+ }
29
+ return match;
30
+ });
16
31
  return src;
17
32
  }
@@ -2,11 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
4
  const legacy_tokens_1 = require("../config/legacy.tokens");
5
- const token_regex_1 = require("../config/token-regex");
6
5
  function transformer(file) {
7
6
  let src = file.source;
8
7
  for (const [name, config] of Object.entries(legacy_tokens_1.legacyTokenConfig)) {
9
- if (!config.twOld || !config.twNew) {
8
+ if (!config.twOld || !config.twNew || !config.regexes.tailwind) {
10
9
  continue;
11
10
  }
12
11
  const isBreakpoint = name.includes("breakpoint");
@@ -16,18 +15,18 @@ function transformer(file) {
16
15
  }
17
16
  const beforeSplit = config.twOld.split(",");
18
17
  const afterSplit = config.twNew.split(",");
19
- const matches = src.match(config.regexes.tailwind) || [];
20
- for (const match of matches) {
21
- const index = beforeSplit.indexOf(match.trim().replace(":", ""));
18
+ src = src.replace(config.regexes.tailwind, (match) => {
19
+ const trimmed = match.trim();
20
+ const cleanToken = trimmed.replace(":", "");
21
+ const index = beforeSplit.indexOf(cleanToken);
22
22
  if (index >= 0) {
23
- const withPrefix = match.trim().startsWith(":");
23
+ const withPrefix = trimmed.startsWith(":");
24
24
  const addSpace = match.startsWith(" ");
25
25
  const replacementToken = afterSplit[index];
26
- src = src.replace((0, token_regex_1.createSingleTwRegex)(match), withPrefix
27
- ? `:${replacementToken}`
28
- : `${addSpace ? " " : ""}${replacementToken}`);
26
+ return `${addSpace ? " " : ""}${withPrefix ? ":" : ""}${replacementToken}`;
29
27
  }
30
- }
28
+ return match;
29
+ });
31
30
  }
32
31
  return src;
33
32
  }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  };
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  const chalk_1 = __importDefault(require("chalk"));
17
+ const commander_1 = require("commander");
17
18
  const node_fs_1 = __importDefault(require("node:fs"));
18
19
  const index_1 = require("./codemod/index");
19
20
  const darkside_1 = require("./darkside");
@@ -21,26 +22,24 @@ const help_1 = require("./help");
21
22
  run();
22
23
  function run() {
23
24
  return __awaiter(this, void 0, void 0, function* () {
24
- if (!process.argv[2] || process.argv[2] === "help") {
25
+ const pkg = JSON.parse(node_fs_1.default.readFileSync("./package.json").toString()).version;
26
+ const program = new commander_1.Command();
27
+ program.version(pkg, "-v, --version");
28
+ program.allowUnknownOption().helpOption(false);
29
+ program.parse();
30
+ const args = program.args;
31
+ if (args.length === 0 || args[0] === "help") {
25
32
  (0, help_1.helpCommand)();
26
33
  return;
27
34
  }
28
- /**
29
- * Runs custom tooling for migrating to v8 tokens
30
- */
31
- if (process.argv[2] === "codemod" && process.argv[3] === "v8-tokens") {
32
- (0, darkside_1.darksideCommand)();
33
- return;
34
- }
35
- if (process.argv[2] === "codemod") {
35
+ if (args[0] === "codemod") {
36
+ if (args.includes("v8-tokens")) {
37
+ (0, darkside_1.darksideCommand)();
38
+ return;
39
+ }
36
40
  (0, index_1.codemodCommand)();
37
41
  return;
38
42
  }
39
- if (process.argv[2] === "-v" || process.argv[2] === "--version") {
40
- const pkg = JSON.parse(node_fs_1.default.readFileSync("./package.json").toString()).version;
41
- console.info(pkg);
42
- return;
43
- }
44
- console.info(chalk_1.default.red(`Unknown command: ${process.argv[2]}.\nRun ${chalk_1.default.cyan("npx @navikt/aksel help")} for all available commands.`));
43
+ console.info(chalk_1.default.red(`Unknown command: ${args[0]}.\nRun ${chalk_1.default.cyan("npx @navikt/aksel help")} for all available commands.`));
45
44
  });
46
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navikt/aksel",
3
- "version": "7.35.0",
3
+ "version": "7.35.1",
4
4
  "description": "Aksel command line interface. Codemods and other utilities for Aksel users.",
5
5
  "author": "Aksel | Nav designsystem team",
6
6
  "license": "MIT",
@@ -32,11 +32,12 @@
32
32
  "url": "https://github.com/navikt/aksel/issues"
33
33
  },
34
34
  "dependencies": {
35
- "@navikt/ds-css": "^7.35.0",
36
- "@navikt/ds-tokens": "^7.35.0",
35
+ "@navikt/ds-css": "^7.35.1",
36
+ "@navikt/ds-tokens": "^7.35.1",
37
37
  "axios": "1.13.2",
38
- "chalk": "4.1.0",
38
+ "chalk": "5.6.2",
39
39
  "cli-progress": "^3.12.0",
40
+ "clipboardy": "^5.0.0",
40
41
  "commander": "10.0.1",
41
42
  "enquirer": "^2.3.6",
42
43
  "fast-glob": "3.2.11",
@@ -55,7 +56,7 @@
55
56
  },
56
57
  "sideEffects": false,
57
58
  "engines": {
58
- "node": ">=16.0.0"
59
+ "node": ">=20.0.0"
59
60
  },
60
61
  "publishConfig": {
61
62
  "access": "public",