@opetope/lint 0.4.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @opetope/lint
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 410f2f0: Run authentic Calls in headless model tests with `runCall` from `@opetope/react/testing`, preserving their existing policies, cancellation, errors and owner lifetime.
8
+
9
+ Add the recommended `opetope/no-internal-imports` rule for static imports, re-exports, literal dynamic imports, unshadowed `require` and TypeScript import types. `configs.internalImports` uses this rule for JavaScript and TypeScript, including tests, without replacing the host's `no-restricted-imports`. The redundant `internalImportPattern` export is removed; allow trusted implementation files through the config or an explicit rule override.
10
+
3
11
  ## 0.4.0
4
12
 
5
13
  No changes in this release.
package/README.md CHANGED
@@ -43,7 +43,7 @@ both it and `eslint` are peer dependencies.
43
43
  ### `recommended`
44
44
 
45
45
  Every rule that holds wherever Opetope is written: `when-predicate`, `define-feature-property-order`,
46
- `require-literal-id`, `id-naming`, `no-snapshot-read-in-render` and `require-declared-models` as errors. A rule that needs to know where a
46
+ `require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render` and `require-declared-models` as errors. A rule that needs to know where a
47
47
  host keeps its files is off here and arrives through `layers`; `prefer-model-selection` is off because the number
48
48
  of hooks a component may keep is a taste a project settles for itself.
49
49
 
@@ -68,27 +68,36 @@ because forgetting one is how a subscription outlives its reader.
68
68
 
69
69
  ### `internalImports`
70
70
 
71
- `opetope.configs.internalImports({ allow, files })` keeps `@opetope/*/internal` out of the code that is neither the
72
- runtime nor its host integration. `allow` names the files that do that work — the restriction does not reach them,
73
- so whatever else the project restricts there stays in force — and `files` is what the restriction covers, by
74
- default every `.ts` and `.tsx`.
71
+ `opetope.configs.internalImports({ allow, files })` configures `opetope/no-internal-imports` across JavaScript and
72
+ TypeScript, including `.mjs` model tests. The rule is already enabled by `recommended`; this helper is useful for
73
+ scoping it or naming trusted library implementation and host integration files. `allow` explicitly switches off
74
+ only this rule for those files. Place the helper's blocks **after** `recommended`: a later `recommended` block
75
+ re-enables the rule, including for the allowed files. Other project restrictions stay intact.
75
76
 
76
77
  ```js
77
- ...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
78
+ export default [
79
+ opetope.configs.recommended,
80
+ ...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
81
+ ];
78
82
  ```
79
83
 
80
- `no-restricted-imports` is a core rule with one value per file, and in a flat config the last entry that names it
81
- wins. A project that already configures that rule takes `opetope.internalImportPattern` into its own entry instead
82
- of spreading this configuration, which would replace that entry for every file it covers:
84
+ The dedicated rule never changes `no-restricted-imports`. The former `internalImportPattern` export is removed:
85
+ enable this rule or use the helper instead of merging an Opetope pattern into the host's restrictions (D261).
83
86
 
84
- ```js
85
- 'no-restricted-imports': ['error', { paths: myPaths, patterns: [...myPatterns, opetope.internalImportPattern] }],
86
- ```
87
+ ## Rules
87
88
 
88
- The files that integrate the host then get the same entry without that one pattern, so they keep every other
89
- restriction the project makes.
89
+ ### `no-internal-imports`
90
90
 
91
- ## Rules
91
+ Application code and tests import public Opetope entries. The rule rejects `@opetope/*/internal` and its deeper
92
+ paths in imports, re-exports, literal `import()` calls, unshadowed `require()` calls, TypeScript import types and
93
+ `import = require`. It includes type-only imports; an internal type is still coupled to the private ABI.
94
+ Use `runCall` from `@opetope/react/testing` to run a real model Call, and `command` to construct a fixture Call.
95
+ Tests do not receive an automatic exception. Trusted library implementation or host integration can use an explicit
96
+ file override; the rule has no autofix because the correct public replacement depends on the imported operation.
97
+
98
+ **The boundary.** This is a syntactic check of literal module paths and templates without substitutions. It does
99
+ not resolve aliases, computed import paths, re-export graphs or custom loaders. A locally shadowed `require` is not
100
+ treated as Node's loader. Apply the config to every JavaScript/TypeScript source and test glob the project uses.
92
101
 
93
102
  ### `when-predicate`
94
103
 
package/README.ru.md CHANGED
@@ -43,7 +43,7 @@ export default [
43
43
  ### `recommended`
44
44
 
45
45
  Все правила, которые действуют везде, где пишут на Opetope: `when-predicate`, `define-feature-property-order`,
46
- `require-literal-id`, `id-naming`, `no-snapshot-read-in-render` и `require-declared-models` как error. Правило, которому нужно знать, где хост
46
+ `require-literal-id`, `id-naming`, `no-internal-imports`, `no-snapshot-read-in-render` и `require-declared-models` как error. Правило, которому нужно знать, где хост
47
47
  держит свои файлы, здесь выключено и приходит через `layers`; `prefer-model-selection` выключено потому, что число
48
48
  hooks, которое компонент вправе держать, каждый проект решает для себя.
49
49
 
@@ -68,27 +68,37 @@ hooks, которое компонент вправе держать, кажды
68
68
 
69
69
  ### `internalImports`
70
70
 
71
- `opetope.configs.internalImports({ allow, files })` держит `@opetope/*/internal` вне кода, который не является ни
72
- рантаймом, ни его интеграцией с хостом. `allow` называет файлы, которые эту работу делают, — ограничение до них не
73
- доходит, поэтому всё остальное, что проект там запрещает, продолжает действовать, — а `files` задаёт, что
74
- ограничение покрывает; по умолчанию это все `.ts` и `.tsx`.
71
+ `opetope.configs.internalImports({ allow, files })` настраивает `opetope/no-internal-imports` для JavaScript и
72
+ TypeScript, включая `.mjs`-тесты моделей. Правило уже включено в `recommended`; helper полезен для ограничения
73
+ области проверки или явного перечисления доверенных файлов реализации библиотеки и интеграции хоста.
74
+ `allow` выключает только это правило в указанных файлах. Размещайте блоки helper **после** `recommended`:
75
+ последующий блок `recommended` снова включает правило, в том числе для разрешённых файлов. Остальные ограничения
76
+ проекта продолжают действовать.
75
77
 
76
78
  ```js
77
- ...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
79
+ export default [
80
+ opetope.configs.recommended,
81
+ ...opetope.configs.internalImports({ allow: ['src/bootstrap/**/devtools.ts'] }),
82
+ ];
78
83
  ```
79
84
 
80
- `no-restricted-imports` — core-правило с одним значением на файл, и во flat config побеждает последняя запись,
81
- которая его называет. Проект, который уже настраивает это правило, берёт `opetope.internalImportPattern` в свою
82
- запись, а не раскрывает эту конфигурацию: она заменила бы его запись для каждого покрытого файла:
85
+ Отдельное правило не изменяет `no-restricted-imports`. Прежний экспорт `internalImportPattern` удалён:
86
+ включайте это правило или helper вместо добавления паттерна Opetope в ограничения хоста (D261).
83
87
 
84
- ```js
85
- 'no-restricted-imports': ['error', { paths: myPaths, patterns: [...myPatterns, opetope.internalImportPattern] }],
86
- ```
88
+ ## Rules
87
89
 
88
- Файлы, которые интегрируют хост, получают ту же запись без этого одного паттерна и сохраняют все остальные
89
- ограничения проекта.
90
+ ### `no-internal-imports`
90
91
 
91
- ## Rules
92
+ Код приложения и тесты импортируют публичные входы Opetope. Правило запрещает `@opetope/*/internal` и вложенные
93
+ пути в imports, re-exports, литеральных `import()`, незатенённых `require()`, TypeScript import types и
94
+ `import = require`. Type-only импорты тоже проверяются: внутренний тип связывает потребителя с приватным ABI.
95
+ Для запуска настоящего Call модели есть `runCall` из `@opetope/react/testing`, для создания фикстуры — `command`.
96
+ Автоматического исключения для тестов нет. Доверенные файлы реализации библиотеки или интеграции хоста можно
97
+ разрешить явным override; autofix отсутствует, потому что публичная замена зависит от импортированной операции.
98
+
99
+ **Граница.** Проверка синтаксическая: строковые пути модулей и шаблоны без подстановок. Она не разрешает алиасы,
100
+ вычисляемые пути, граф re-exports или пользовательские загрузчики. Локальный `require` не считается загрузчиком
101
+ Node. Применяйте конфигурацию ко всем используемым JavaScript/TypeScript исходникам и тестам.
92
102
 
93
103
  ### `when-predicate`
94
104
 
package/dist/index.d.ts CHANGED
@@ -32,6 +32,9 @@ declare const plugin: {
32
32
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
33
33
  name: string;
34
34
  };
35
+ 'no-internal-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
36
+ name: string;
37
+ };
35
38
  'no-snapshot-read-in-render': import("@typescript-eslint/utils/ts-eslint").RuleModule<"snapshotInRender", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
36
39
  name: string;
37
40
  };
@@ -84,6 +87,7 @@ declare const configuredPlugin: {
84
87
  'opetope/define-feature-property-order': "error";
85
88
  'opetope/id-naming': "error";
86
89
  'opetope/layer-placement': "off";
90
+ 'opetope/no-internal-imports': "error";
87
91
  'opetope/no-snapshot-read-in-render': "error";
88
92
  'opetope/no-subscribe-outside-models': "off";
89
93
  'opetope/prefer-model-selection': "off";
@@ -93,10 +97,6 @@ declare const configuredPlugin: {
93
97
  };
94
98
  };
95
99
  };
96
- internalImportPattern: {
97
- group: string[];
98
- message: string;
99
- };
100
100
  };
101
101
  declare const opetopeLint: typeof configuredPlugin & Pick<ESLint.Plugin, "configs">;
102
102
  export default opetopeLint;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as a}from"./rules/id-naming.js";import{layerPlacement as l}from"./rules/layer-placement.js";import{noSnapshotReadInRender as d}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as m}from"./rules/no-subscribe-outside-models.js";import{preferModelSelection as c}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as u}from"./rules/require-declared-models.js";import{requireLiteralId as f}from"./rules/require-literal-id.js";import{whenPredicate as g}from"./rules/when-predicate.js";const b=["integration","models","ui"],n=["**/*.ts","**/*.tsx"],h=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],y={meta:{name:"@opetope/lint",version:"0.4.0"},rules:{"define-feature-property-order":p,"id-naming":a,"layer-placement":l,"no-snapshot-read-in-render":d,"no-subscribe-outside-models":m,"prefer-model-selection":c,"require-declared-models":u,"require-literal-id":f,"when-predicate":g}},o=y,I={name:"opetope/recommended",plugins:{opetope:o},rules:{"opetope/define-feature-property-order":"error","opetope/id-naming":"error","opetope/layer-placement":"off","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/when-predicate":"error"}},P=e=>b.flatMap(r=>{const t=e[r]??[];return t.length===0?[]:[{files:[...t],name:`opetope/layers-${r}`,plugins:{opetope:o},rules:{"opetope/layer-placement":["error",{layer:r}]}}]}),S=({models:e=[],tests:r=h})=>e.length===0?[]:[{files:n,name:"opetope/layers-subscriptions",plugins:{opetope:o},rules:{"opetope/no-subscribe-outside-models":"error"}},{files:[...e,...r],name:"opetope/layers-subscriptions-allowed",rules:{"opetope/no-subscribe-outside-models":"off"}}],_=e=>[...P(e),...S(e)],s={group:["@opetope/*/internal"],message:"The internal entries carry the runtime and its host integration. Feature UI, models and data layers import the safe entries."},E=({allow:e=[],files:r=n}={})=>[{files:[...r],...e.length===0?{}:{ignores:[...e]},name:"opetope/internal-imports",rules:{"no-restricted-imports":["error",{patterns:[s]}]}}],R=Object.assign(o,{configs:{internalImports:E,layers:_,recommended:I},internalImportPattern:s}),i=R;export{i as default,i as opetopeLint};
1
+ import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as s}from"./rules/id-naming.js";import{layerPlacement as i}from"./rules/layer-placement.js";import{noInternalImports as l}from"./rules/no-internal-imports.js";import{noSnapshotReadInRender as m}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as a}from"./rules/no-subscribe-outside-models.js";import{preferModelSelection as d}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as c}from"./rules/require-declared-models.js";import{requireLiteralId as u}from"./rules/require-literal-id.js";import{whenPredicate as f}from"./rules/when-predicate.js";const g=["integration","models","ui"],b=["**/*.ts","**/*.tsx"],h=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],y={meta:{name:"@opetope/lint",version:"0.5.0"},rules:{"define-feature-property-order":p,"id-naming":s,"layer-placement":i,"no-internal-imports":l,"no-snapshot-read-in-render":m,"no-subscribe-outside-models":a,"prefer-model-selection":d,"require-declared-models":c,"require-literal-id":u,"when-predicate":f}},r=y,S={name:"opetope/recommended",plugins:{opetope:r},rules:{"opetope/define-feature-property-order":"error","opetope/id-naming":"error","opetope/layer-placement":"off","opetope/no-internal-imports":"error","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/when-predicate":"error"}},I=e=>g.flatMap(o=>{const t=e[o]??[];return t.length===0?[]:[{files:[...t],name:`opetope/layers-${o}`,plugins:{opetope:r},rules:{"opetope/layer-placement":["error",{layer:o}]}}]}),q=({models:e=[],tests:o=h})=>e.length===0?[]:[{files:b,name:"opetope/layers-subscriptions",plugins:{opetope:r},rules:{"opetope/no-subscribe-outside-models":"error"}},{files:[...e,...o],name:"opetope/layers-subscriptions-allowed",rules:{"opetope/no-subscribe-outside-models":"off"}}],_=e=>[...I(e),...q(e)],j=({allow:e=[],files:o=["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]}={})=>[{files:[...o],name:"opetope/internal-imports",plugins:{opetope:r},rules:{"opetope/no-internal-imports":"error"}},...e.length===0?[]:[{files:[...e],name:"opetope/internal-imports-allowed",rules:{"opetope/no-internal-imports":"off"}}]],x=Object.assign(r,{configs:{internalImports:j,layers:_,recommended:S}}),n=x;export{n as default,n as opetopeLint};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.4.0' },\n rules: {\n 'define-feature-property-order': defineFeaturePropertyOrder,\n 'id-naming': idNaming,\n 'layer-placement': layerPlacement,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'when-predicate': whenPredicate,\n },\n};\n\n/**\n * The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that\n * needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads\n * `opetope/when-predicate` wherever a host names it.\n */\n// Rules use the parser's typed AST internally. The public bridge exposes the\n// supported ESLint rule API; package smoke tests execute it under both linters.\ntype NativeRule = NonNullable<ESLint.Plugin['rules']>[string];\ntype NativeRules = {\n [Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;\n};\nconst nativePlugin = plugin as unknown as { meta: typeof plugin.meta; rules: NativeRules };\n\nconst recommended = {\n name: 'opetope/recommended',\n plugins: { opetope: nativePlugin },\n rules: {\n 'opetope/define-feature-property-order': 'error',\n 'opetope/id-naming': 'error',\n 'opetope/layer-placement': 'off',\n 'opetope/no-snapshot-read-in-render': 'error',\n 'opetope/no-subscribe-outside-models': 'off',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/when-predicate': 'error',\n },\n} satisfies Linter.Config;\n\n/**\n * Placement is a convention of the project that names its layers, not a law of the framework: a layer without a\n * glob is a layer this host does not have, and it produces no configuration.\n */\nconst placement = (options: LayerOptions): readonly Linter.Config[] =>\n LAYERS.flatMap(layer => {\n const files = options[layer] ?? [];\n\n if (files.length === 0) return [];\n\n return [\n {\n files: [...files],\n name: `opetope/layers-${layer}`,\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/layer-placement': ['error', { layer }] },\n } satisfies Linter.Config,\n ];\n });\n\n/**\n * A subscription is written where the state lives, so this pair arrives only with the model layer that owns it. The\n * restriction covers every source file and steps aside in that layer and in tests: a file no layer claims is\n * covered rather than forgotten.\n */\nconst subscriptions = ({ models = [], tests = TEST_FILES }: LayerOptions): readonly Linter.Config[] =>\n models.length === 0\n ? []\n : [\n {\n files: SOURCE_FILES,\n name: 'opetope/layers-subscriptions',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-subscribe-outside-models': 'error' },\n },\n {\n files: [...models, ...tests],\n name: 'opetope/layers-subscriptions-allowed',\n rules: { 'opetope/no-subscribe-outside-models': 'off' },\n },\n ];\n\nconst layers = (options: LayerOptions): readonly Linter.Config[] => [...placement(options), ...subscriptions(options)];\n\n/**\n * The one restriction this package expresses through a core rule rather than a rule of its own, so a project that\n * already configures `no-restricted-imports` adds this entry to its own list instead of losing it.\n */\nconst INTERNAL_IMPORT_PATTERN = {\n group: ['@opetope/*/internal'],\n message:\n 'The internal entries carry the runtime and its host integration. Feature UI, models and data layers import the safe entries.',\n};\n\n/**\n * The internal entries are for runtime implementation and host integration. `allow` names the files that do that\n * work; the restriction simply does not reach them, so whatever else a project restricts there stays in force.\n *\n * `no-restricted-imports` is a core rule with one value per file: in a flat config the last entry that names it\n * wins. A project that already configures it takes `internalImportPattern` into that entry instead of spreading\n * this config, which would replace the entry for every file it covers.\n */\nconst internalImports = ({\n allow = [],\n files = SOURCE_FILES,\n}: InternalImportOptions = {}): readonly Linter.Config[] => [\n {\n files: [...files],\n ...(allow.length === 0 ? {} : { ignores: [...allow] }),\n name: 'opetope/internal-imports',\n rules: { 'no-restricted-imports': ['error', { patterns: [INTERNAL_IMPORT_PATTERN] }] },\n },\n];\n\nconst configuredPlugin = Object.assign(nativePlugin, {\n configs: { internalImports, layers, recommended },\n internalImportPattern: INTERNAL_IMPORT_PATTERN,\n});\n\n// ESLint consumes the rule map. Config factories are additional plugin helpers;\n// expose both contracts without requiring casts at native flat-config call sites.\n// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Required by native ESLint config typetests.\nconst opetopeLint = configuredPlugin as unknown as typeof configuredPlugin & Pick<ESLint.Plugin, 'configs'>;\n\n/*\n * Two shapes of one object. The default is what a tool reads when it loads a plugin by path — Oxlint's `jsPlugins`\n * takes `(await import(path)).default` — and what an ESLint flat config imports by convention; the named export is\n * for a config that prefers to say which binding it takes.\n */\n// oxlint-disable-next-line import/no-default-export\n// eslint-disable-next-line import-x/no-default-export\nexport default opetopeLint;\nexport { opetopeLint };\n"],"names":["LAYERS","SOURCE_FILES","TEST_FILES","plugin","defineFeaturePropertyOrder","idNaming","layerPlacement","noSnapshotReadInRender","noSubscribeOutsideModels","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","INTERNAL_IMPORT_PATTERN","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"knBA4BA,MAAMA,EAAS,CAAC,cAAe,SAAU,IAAI,EACvCC,EAAe,CAAC,UAAW,UAAU,EACrCC,EAAa,CAAC,kBAAmB,eAAgB,eAAe,EAEhEC,EAAS,CACb,KAAM,CAAE,KAAM,gBAAiB,QAAS,OAAO,EAC/C,MAAO,CACL,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAeV,EAEfW,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,qCAAsC,QACtC,sCAAuC,MACvC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBhB,EAAO,QAAQiB,GAAQ,CACrB,MAAMC,EAAQF,EAAQC,CAAK,GAAK,CAAA,EAEhC,OAAIC,EAAM,SAAW,EAAU,CAAA,EAExB,CACL,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,kBAAkBD,CAAK,GAC7B,QAAS,CAAE,QAASJ,CAAY,EAChC,MAAO,CAAE,0BAA2B,CAAC,QAAS,CAAE,MAAAI,CAAK,CAAE,CAAC,CACjC,EAE7B,CAAC,EAOGE,EAAgB,CAAC,CAAE,OAAAC,EAAS,CAAA,EAAI,MAAAC,EAAQnB,CAAU,IACtDkB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOnB,EACP,KAAM,+BACN,QAAS,CAAE,QAASY,CAAY,EAChC,MAAO,CAAE,sCAAuC,OAAO,CACxD,EACD,CACE,MAAO,CAAC,GAAGO,EAAQ,GAAGC,CAAK,EAC3B,KAAM,uCACN,MAAO,CAAE,sCAAuC,KAAK,CACtD,GAGHC,EAAUN,GAAoD,CAAC,GAAGD,EAAUC,CAAO,EAAG,GAAGG,EAAcH,CAAO,CAAC,EAM/GO,EAA0B,CAC9B,MAAO,CAAC,qBAAqB,EAC7B,QACE,gIAWEC,EAAkB,CAAC,CACvB,MAAAC,EAAQ,CAAA,EACR,MAAAP,EAAQjB,CAAY,EACK,KAAiC,CAC1D,CACE,MAAO,CAAC,GAAGiB,CAAK,EAChB,GAAIO,EAAM,SAAW,EAAI,CAAA,EAAK,CAAE,QAAS,CAAC,GAAGA,CAAK,GAClD,KAAM,2BACN,MAAO,CAAE,wBAAyB,CAAC,QAAS,CAAE,SAAU,CAACF,CAAuB,CAAC,CAAE,CAAC,CACrF,GAGGG,EAAmB,OAAO,OAAOb,EAAc,CACnD,QAAS,CAAE,gBAAAW,EAAiB,OAAAF,EAAQ,YAAAR,CAAW,EAC/C,sBAAuBS,CACxB,CAAA,EAKKI,EAAcD"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noInternalImports } from './rules/no-internal-imports';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.5.0' },\n rules: {\n 'define-feature-property-order': defineFeaturePropertyOrder,\n 'id-naming': idNaming,\n 'layer-placement': layerPlacement,\n 'no-internal-imports': noInternalImports,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'when-predicate': whenPredicate,\n },\n};\n\n/**\n * The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that\n * needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads\n * `opetope/when-predicate` wherever a host names it.\n */\n// Rules use the parser's typed AST internally. The public bridge exposes the\n// supported ESLint rule API; package smoke tests execute it under both linters.\ntype NativeRule = NonNullable<ESLint.Plugin['rules']>[string];\ntype NativeRules = {\n [Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;\n};\nconst nativePlugin = plugin as unknown as { meta: typeof plugin.meta; rules: NativeRules };\n\nconst recommended = {\n name: 'opetope/recommended',\n plugins: { opetope: nativePlugin },\n rules: {\n 'opetope/define-feature-property-order': 'error',\n 'opetope/id-naming': 'error',\n 'opetope/layer-placement': 'off',\n 'opetope/no-internal-imports': 'error',\n 'opetope/no-snapshot-read-in-render': 'error',\n 'opetope/no-subscribe-outside-models': 'off',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/when-predicate': 'error',\n },\n} satisfies Linter.Config;\n\n/**\n * Placement is a convention of the project that names its layers, not a law of the framework: a layer without a\n * glob is a layer this host does not have, and it produces no configuration.\n */\nconst placement = (options: LayerOptions): readonly Linter.Config[] =>\n LAYERS.flatMap(layer => {\n const files = options[layer] ?? [];\n\n if (files.length === 0) return [];\n\n return [\n {\n files: [...files],\n name: `opetope/layers-${layer}`,\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/layer-placement': ['error', { layer }] },\n } satisfies Linter.Config,\n ];\n });\n\n/**\n * A subscription is written where the state lives, so this pair arrives only with the model layer that owns it. The\n * restriction covers every source file and steps aside in that layer and in tests: a file no layer claims is\n * covered rather than forgotten.\n */\nconst subscriptions = ({ models = [], tests = TEST_FILES }: LayerOptions): readonly Linter.Config[] =>\n models.length === 0\n ? []\n : [\n {\n files: SOURCE_FILES,\n name: 'opetope/layers-subscriptions',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-subscribe-outside-models': 'error' },\n },\n {\n files: [...models, ...tests],\n name: 'opetope/layers-subscriptions-allowed',\n rules: { 'opetope/no-subscribe-outside-models': 'off' },\n },\n ];\n\nconst layers = (options: LayerOptions): readonly Linter.Config[] => [...placement(options), ...subscriptions(options)];\n\n/**\n * Only library implementation and explicitly named host integration can cross the internal ABI. A dedicated rule\n * leaves the project's `no-restricted-imports` untouched and also covers literal dynamic imports in JS tests.\n * Allowed files explicitly override `recommended`, which enables this rule as well.\n */\nconst internalImports = ({\n allow = [],\n files = ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],\n}: InternalImportOptions = {}): readonly Linter.Config[] => [\n {\n files: [...files],\n name: 'opetope/internal-imports',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-internal-imports': 'error' },\n },\n ...(allow.length === 0\n ? []\n : [\n {\n files: [...allow],\n name: 'opetope/internal-imports-allowed',\n rules: { 'opetope/no-internal-imports': 'off' as const },\n },\n ]),\n];\n\nconst configuredPlugin = Object.assign(nativePlugin, {\n configs: { internalImports, layers, recommended },\n});\n\n// ESLint consumes the rule map. Config factories are additional plugin helpers;\n// expose both contracts without requiring casts at native flat-config call sites.\n// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Required by native ESLint config typetests.\nconst opetopeLint = configuredPlugin as unknown as typeof configuredPlugin & Pick<ESLint.Plugin, 'configs'>;\n\n/*\n * Two shapes of one object. The default is what a tool reads when it loads a plugin by path — Oxlint's `jsPlugins`\n * takes `(await import(path)).default` — and what an ESLint flat config imports by convention; the named export is\n * for a config that prefers to say which binding it takes.\n */\n// oxlint-disable-next-line import/no-default-export\n// eslint-disable-next-line import-x/no-default-export\nexport default opetopeLint;\nexport { opetopeLint };\n"],"names":["LAYERS","SOURCE_FILES","TEST_FILES","plugin","defineFeaturePropertyOrder","idNaming","layerPlacement","noInternalImports","noSnapshotReadInRender","noSubscribeOutsideModels","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"qrBA6BA,MAAMA,EAAS,CAAC,cAAe,SAAU,IAAI,EACvCC,EAAe,CAAC,UAAW,UAAU,EACrCC,EAAa,CAAC,kBAAmB,eAAgB,eAAe,EAEhEC,EAAS,CACb,KAAM,CAAE,KAAM,gBAAiB,QAAS,OAAO,EAC/C,MAAO,CACL,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,sBAAuBC,EACvB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAeX,EAEfY,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,8BAA+B,QAC/B,qCAAsC,QACtC,sCAAuC,MACvC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBjB,EAAO,QAAQkB,GAAQ,CACrB,MAAMC,EAAQF,EAAQC,CAAK,GAAK,CAAA,EAEhC,OAAIC,EAAM,SAAW,EAAU,CAAA,EAExB,CACL,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,kBAAkBD,CAAK,GAC7B,QAAS,CAAE,QAASJ,CAAY,EAChC,MAAO,CAAE,0BAA2B,CAAC,QAAS,CAAE,MAAAI,CAAK,CAAE,CAAC,CACjC,EAE7B,CAAC,EAOGE,EAAgB,CAAC,CAAE,OAAAC,EAAS,CAAA,EAAI,MAAAC,EAAQpB,CAAU,IACtDmB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOpB,EACP,KAAM,+BACN,QAAS,CAAE,QAASa,CAAY,EAChC,MAAO,CAAE,sCAAuC,OAAO,CACxD,EACD,CACE,MAAO,CAAC,GAAGO,EAAQ,GAAGC,CAAK,EAC3B,KAAM,uCACN,MAAO,CAAE,sCAAuC,KAAK,CACtD,GAGHC,EAAUN,GAAoD,CAAC,GAAGD,EAAUC,CAAO,EAAG,GAAGG,EAAcH,CAAO,CAAC,EAO/GO,EAAkB,CAAC,CACvB,MAAAC,EAAQ,CAAA,EACR,MAAAN,EAAQ,CAAC,sCAAsC,CAAC,EACvB,KAAiC,CAC1D,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,2BACN,QAAS,CAAE,QAASL,CAAY,EAChC,MAAO,CAAE,8BAA+B,OAAO,CAChD,EACD,GAAIW,EAAM,SAAW,EACjB,CAAA,EACA,CACE,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,mCACN,MAAO,CAAE,8BAA+B,KAAc,CACvD,IAIHC,EAAmB,OAAO,OAAOZ,EAAc,CACnD,QAAS,CAAE,gBAAAU,EAAiB,OAAAD,EAAQ,YAAAR,CAAW,CAChD,CAAA,EAKKY,EAAcD"}
@@ -0,0 +1,4 @@
1
+ declare const noInternalImports: import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ export { noInternalImports };
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as i,ASTUtils as a}from"@typescript-eslint/utils";import{createRule as l}from"../rule.js";const s=/^@opetope\/[^/]+\/internal(?:\/|$)/;function n(r){if(r.type===i.TSLiteralType)return n(r.literal);if(r.type===i.Literal&&typeof r.value=="string")return r.value;if(r.type===i.TemplateLiteral&&r.expressions.length===0)return r.quasis[0]?.value.cooked}function o(r){return r.argument??r.source??null}const p=l({create(r){const t=e=>{e!==null&&s.test(n(e)??"")&&r.report({messageId:"internalImport",node:e})};return{CallExpression(e){e.callee.type!==i.Identifier||e.callee.name!=="require"||(a.findVariable(r.sourceCode.getScope(e),e.callee)?.defs.length??0)>0||t(e.arguments[0]??null)},ExportAllDeclaration:e=>t(e.source),ExportNamedDeclaration:e=>t(e.source),ImportDeclaration:e=>t(e.source),ImportExpression:e=>t(e.source),TSExternalModuleReference:e=>t(e.expression),TSImportType:e=>t(o(e))}},meta:{docs:{description:"Application code and tests use public Opetope entries instead of the internal ABI."},messages:{internalImport:"Opetope internal entries are reserved for library implementation and explicit host integration. Use public entries; tests run real Calls with runCall from @opetope/react/testing."},schema:[],type:"problem"},name:"no-internal-imports"});export{p as noInternalImports};
2
+ //# sourceMappingURL=no-internal-imports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-internal-imports.js","sources":["../../src/rules/no-internal-imports.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';\n\nimport { createRule } from '../rule';\n\n/** Match the package boundary, including deep paths, without claiming unrelated packages or `internals`. */\nconst internalEntry = /^@opetope\\/[^/]+\\/internal(?:\\/|$)/;\n\nfunction sourceText(node: TSESTree.Node): string | null | undefined {\n if (node.type === AST_NODE_TYPES.TSLiteralType) return sourceText(node.literal);\n if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') return node.value;\n if (node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0)\n return node.quasis[0]?.value.cooked;\n return undefined;\n}\n\n/** TypeScript-ESLint calls this `argument`; Oxlint's ESTree bridge exposes the same source as `source`. */\nfunction importTypeSource(node: {\n readonly argument?: TSESTree.Node;\n readonly source?: TSESTree.Node;\n}): TSESTree.Node | null {\n return node.argument ?? node.source ?? null;\n}\n\nconst noInternalImports = createRule<[], 'internalImport'>({\n create(context) {\n const inspect = (node: TSESTree.Node | null): void => {\n if (node !== null && internalEntry.test(sourceText(node) ?? ''))\n context.report({ messageId: 'internalImport', node });\n };\n return {\n CallExpression(node): void {\n if (node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== 'require') return;\n const variable = ASTUtils.findVariable(context.sourceCode.getScope(node), node.callee);\n if ((variable?.defs.length ?? 0) > 0) return;\n inspect(node.arguments[0] ?? null);\n },\n ExportAllDeclaration: node => inspect(node.source),\n ExportNamedDeclaration: node => inspect(node.source),\n ImportDeclaration: node => inspect(node.source),\n ImportExpression: node => inspect(node.source),\n TSExternalModuleReference: node => inspect(node.expression),\n TSImportType: node => inspect(importTypeSource(node)),\n };\n },\n meta: {\n docs: { description: 'Application code and tests use public Opetope entries instead of the internal ABI.' },\n messages: {\n internalImport:\n 'Opetope internal entries are reserved for library implementation and explicit host integration. Use public entries; tests run real Calls with runCall from @opetope/react/testing.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-internal-imports',\n});\n\nexport { noInternalImports };\n"],"names":["internalEntry","sourceText","node","AST_NODE_TYPES","importTypeSource","noInternalImports","createRule","context","inspect","ASTUtils"],"mappings":"gHAMA,MAAMA,EAAgB,qCAEtB,SAASC,EAAWC,EAAmB,CACrC,GAAIA,EAAK,OAASC,EAAe,cAAe,OAAOF,EAAWC,EAAK,OAAO,EAC9E,GAAIA,EAAK,OAASC,EAAe,SAAW,OAAOD,EAAK,OAAU,SAAU,OAAOA,EAAK,MACxF,GAAIA,EAAK,OAASC,EAAe,iBAAmBD,EAAK,YAAY,SAAW,EAC9E,OAAOA,EAAK,OAAO,CAAC,GAAG,MAAM,MAEjC,CAGA,SAASE,EAAiBF,EAGzB,CACC,OAAOA,EAAK,UAAYA,EAAK,QAAU,IACzC,CAEA,MAAMG,EAAoBC,EAAiC,CACzD,OAAOC,EAAO,CACZ,MAAMC,EAAWN,GAAoC,CAC/CA,IAAS,MAAQF,EAAc,KAAKC,EAAWC,CAAI,GAAK,EAAE,GAC5DK,EAAQ,OAAO,CAAE,UAAW,iBAAkB,KAAAL,CAAI,CAAE,CACxD,EACA,MAAO,CACL,eAAeA,EAAI,CACbA,EAAK,OAAO,OAASC,EAAe,YAAcD,EAAK,OAAO,OAAS,YAC1DO,EAAS,aAAaF,EAAQ,WAAW,SAASL,CAAI,EAAGA,EAAK,MAAM,GACtE,KAAK,QAAU,GAAK,GACnCM,EAAQN,EAAK,UAAU,CAAC,GAAK,IAAI,CACnC,EACA,qBAAsBA,GAAQM,EAAQN,EAAK,MAAM,EACjD,uBAAwBA,GAAQM,EAAQN,EAAK,MAAM,EACnD,kBAAmBA,GAAQM,EAAQN,EAAK,MAAM,EAC9C,iBAAkBA,GAAQM,EAAQN,EAAK,MAAM,EAC7C,0BAA2BA,GAAQM,EAAQN,EAAK,UAAU,EAC1D,aAAcA,GAAQM,EAAQJ,EAAiBF,CAAI,CAAC,EAExD,EACA,KAAM,CACJ,KAAM,CAAE,YAAa,oFAAoF,EACzG,SAAU,CACR,eACE,oLACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,qBACP,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opetope/lint",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "engines": {
5
5
  "node": ">=20.19.0"
6
6
  },