@jterrazz/typescript 10.1.5 → 10.1.6

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/bin/typescript.sh CHANGED
@@ -17,14 +17,17 @@ else
17
17
  PROJECT_ROOT=$(pwd)
18
18
  fi
19
19
 
20
- # Get the real directory where this script lives (resolve symlinks)
20
+ # The PHYSICAL directory this script lives in, symlinks resolved on every hop.
21
+ # `cd -P` matters: under pnpm the package sits behind a symlinked prefix, and
22
+ # a shim found from the logical path execs a relative target that lands
23
+ # beside the wrong parent.
21
24
  SCRIPT_PATH="$0"
22
25
  while [ -L "$SCRIPT_PATH" ]; do
23
- LINK_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
26
+ LINK_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)"
24
27
  SCRIPT_PATH="$(readlink "$SCRIPT_PATH")"
25
28
  [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$LINK_DIR/$SCRIPT_PATH"
26
29
  done
27
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
30
+ SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)"
28
31
  PACKAGE_ROOT="$SCRIPT_DIR/.."
29
32
 
30
33
  # Find binaries - check package's node_modules first, then project's (handles npm hoisting)
@@ -63,6 +63,13 @@ const SHORTCUTS = new Set([
63
63
  */
64
64
  const SWEPT = new Set(['apps', 'bin', 'lib', 'packages', 'specs', 'src', 'tests']);
65
65
 
66
+ /**
67
+ * A Next.js route segment is a URL: what a directory below `app/` is called is
68
+ * the address the product serves, not a name the tree chose. The router root
69
+ * itself is judged like any directory; what it contains is not.
70
+ */
71
+ const ROUTER_ROOT = 'app';
72
+
66
73
  /** A `_` opens a row — a fixture, a golden, a template — and a row is not a subject. */
67
74
  const isOfTheRow = (name) => name.startsWith('_');
68
75
 
@@ -81,7 +88,8 @@ const stemOf = (name) => name.split('.')[0] ?? '';
81
88
 
82
89
  /**
83
90
  * Every name a set of paths puts on the tree, each mapped to the path that
84
- * carries it: every directory below a swept root, and every file's stem.
91
+ * carries it: every directory below a swept root that is not a route, and
92
+ * every file's stem.
85
93
  */
86
94
  function namesOf(files) {
87
95
  const named = new Map();
@@ -92,8 +100,11 @@ function namesOf(files) {
92
100
  continue;
93
101
  }
94
102
 
95
- /* From the second segment on: the root's own name is not the project's. */
96
- for (let index = 1; index < segments.length - 1; index += 1) {
103
+ /* From the second segment on: the root's own name is not the project's,
104
+ * and below a router root a directory is a route. */
105
+ const routerRoot = segments.indexOf(ROUTER_ROOT, 1);
106
+ const lastJudged = routerRoot === -1 ? segments.length - 1 : routerRoot + 1;
107
+ for (let index = 1; index < Math.min(lastJudged, segments.length - 1); index += 1) {
97
108
  const path = segments.slice(0, index + 1).join('/');
98
109
  named.set(path, segments[index]);
99
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "10.1.5",
3
+ "version": "10.1.6",
4
4
  "license": "MIT",
5
5
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
6
6
  "repository": {
@@ -22,9 +22,25 @@ import { fragment, on, scoped } from '../_contract.js';
22
22
 
23
23
  /**
24
24
  * Build a fragment from a layer map. Each layer is
25
- * `{ name, files, deny, allow?, message }`: `files` are the globs that BELONG
26
- * to the layer, `deny` the specifier globs it may not import, `allow` the
27
- * exceptions carved out of them.
25
+ * `{ name, files, deny, allow?, allowTypeImports?, message }`: `files` are the
26
+ * globs that BELONG to the layer, `deny` the specifier globs it may not
27
+ * import, `allow` the exceptions carved out of them, and `allowTypeImports`
28
+ * lets a type-only import cross — a type is a contract, not a dependency at
29
+ * runtime.
30
+ */
31
+ /**
32
+ * @typedef {object} Layer One row of a layer map.
33
+ * @property {string} name What the layer is called in a refusal.
34
+ * @property {readonly string[]} files The globs that belong to the layer.
35
+ * @property {readonly string[]} deny The specifier globs it may not import.
36
+ * @property {readonly string[]} [allow] Exceptions carved out of `deny`.
37
+ * @property {boolean} [allowTypeImports] Whether a type-only import may cross.
38
+ * @property {string} message What a refusal says.
39
+ */
40
+
41
+ /**
42
+ * @param {{ id?: string, map: readonly Layer[] }} definition The map, and the id the fragment answers to.
43
+ * @returns {Omit<ReturnType<typeof fragment>, 'overrides'> & { overrides: readonly import('../_contract.js').Scoped[] }} The fragment, one override per layer.
28
44
  */
29
45
  export function layers({ id = 'layers', map }) {
30
46
  assertDisjoint(id, map);
@@ -44,6 +60,9 @@ export function layers({ id = 'layers', map }) {
44
60
  ...(layer.allow ?? []).map((glob) => `!${glob}`),
45
61
  ],
46
62
  message: layer.message,
63
+ ...(layer.allowTypeImports === true
64
+ ? { allowTypeImports: true }
65
+ : {}),
47
66
  },
48
67
  ],
49
68
  },
@@ -0,0 +1,48 @@
1
+ import { expect, test } from 'vitest';
2
+
3
+ import { layers } from './layers.js';
4
+
5
+ const LAYER = {
6
+ deny: ['**/adapters/**'],
7
+ files: ['src/ports/**/*.ts'],
8
+ message: 'a port names no adapter',
9
+ name: 'ports',
10
+ };
11
+
12
+ test('a layer forbids the specifiers it denies and nothing else about its type imports', () => {
13
+ // Given - a layer that says nothing about type imports
14
+ // When - the map is compiled
15
+ const [override] = layers({ map: [LAYER] }).overrides;
16
+
17
+ // Then - the pattern carries the deny glob and no type-import exemption
18
+ expect(override?.decisions['no-restricted-imports']?.options).toStrictEqual([
19
+ { patterns: [{ group: ['**/adapters/**'], message: 'a port names no adapter' }] },
20
+ ]);
21
+ });
22
+
23
+ test('a layer that allows type imports lets a contract cross without its runtime', () => {
24
+ // Given - the same layer with type imports allowed
25
+ // When - the map is compiled
26
+ const [override] = layers({ map: [{ ...LAYER, allowTypeImports: true }] }).overrides;
27
+
28
+ // Then - the pattern says so to oxlint
29
+ expect(override?.decisions['no-restricted-imports']?.options).toStrictEqual([
30
+ {
31
+ patterns: [
32
+ {
33
+ allowTypeImports: true,
34
+ group: ['**/adapters/**'],
35
+ message: 'a port names no adapter',
36
+ },
37
+ ],
38
+ },
39
+ ]);
40
+ });
41
+
42
+ test('two layers claiming one files glob are refused before oxlint erases one of them', () => {
43
+ // Given - two layers on the same glob
44
+ const map = [LAYER, { ...LAYER, name: 'twin' }];
45
+
46
+ // Then - the map is refused by name
47
+ expect(() => layers({ map })).toThrow('belongs to both "ports" and "twin"');
48
+ });