@techninja/clearstack 0.3.6 → 0.3.8

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.
@@ -109,6 +109,50 @@ render: ({ items }) => html`
109
109
 
110
110
  ---
111
111
 
112
+ ## Import Map Aliases
113
+
114
+ All cross-directory imports in browser-facing code use `#prefix/` aliases
115
+ defined in the import map (`src/index.html`). This eliminates fragile
116
+ relative paths like `../../../utils/foo.js`.
117
+
118
+ ### Available Prefixes
119
+
120
+ | Prefix | Resolves to |
121
+ | --------------- | ------------------------------ |
122
+ | `#store/` | `/store/` |
123
+ | `#utils/` | `/utils/` |
124
+ | `#atoms/` | `/components/atoms/` |
125
+ | `#molecules/` | `/components/molecules/` |
126
+ | `#organisms/` | `/components/organisms/` |
127
+ | `#templates/` | `/components/templates/` |
128
+ | `#pages/` | `/pages/` |
129
+
130
+ ### Rules
131
+
132
+ - **No `../` imports.** The spec check (`npm run spec`) flags any `../`
133
+ in browser-facing JS. Use a `#prefix/` alias instead.
134
+ - **Same-directory `./` is fine.** `index.js` re-exports and co-located
135
+ files use `./` naturally.
136
+ - **Server/API files are exempt.** `src/api/` and `src/server.js` run in
137
+ Node, not the browser — they use `./` relative imports.
138
+ - **Test files are exempt.** `*.test.js` files may use relative paths.
139
+ - **jsconfig.json mirrors the import map.** The `paths` entries in
140
+ `.configs/jsconfig.json` let `tsc --checkJs` resolve `#prefix/` imports.
141
+
142
+ ### Examples
143
+
144
+ ```javascript
145
+ // ✅ Good — clear, refactor-safe
146
+ import AppState from '#store/AppState.js';
147
+ import { formatDate } from '#utils/formatDate.js';
148
+ import '#atoms/app-badge/app-badge.js';
149
+
150
+ // ❌ Bad — fragile, hard to read
151
+ import AppState from '../../../store/AppState.js';
152
+ ```
153
+
154
+ ---
155
+
112
156
  ## Error Handling
113
157
 
114
158
  Errors are handled **at the boundary where they are actionable.** Each layer
@@ -193,8 +237,8 @@ state: store(AppState),
193
237
  // In src/utils/filterActive.js
194
238
  export const filterActive = (items) => items.filter(i => i.active);
195
239
 
196
- // In component
197
- import { filterActive } from '../../utils/filterActive.js';
240
+ // In component — use import map alias, never ../
241
+ import { filterActive } from '#utils/filterActive.js';
198
242
  render: ({ items }) => html`<ul>${filterActive(items).map(...)}</ul>`,
199
243
  ```
200
244
 
@@ -121,7 +121,14 @@ declared in `src/index.html`.
121
121
  <script type="importmap">
122
122
  {
123
123
  "imports": {
124
- "hybrids": "/vendor/hybrids/index.js"
124
+ "hybrids": "/vendor/hybrids/index.js",
125
+ "#store/": "/store/",
126
+ "#utils/": "/utils/",
127
+ "#atoms/": "/components/atoms/",
128
+ "#molecules/": "/components/molecules/",
129
+ "#organisms/": "/components/organisms/",
130
+ "#templates/": "/components/templates/",
131
+ "#pages/": "/pages/"
125
132
  }
126
133
  }
127
134
  </script>
@@ -139,6 +146,8 @@ declared in `src/index.html`.
139
146
  2. Import map resolves `'hybrids'` → `/vendor/hybrids/index.js`.
140
147
  3. Server serves the file from `src/vendor/hybrids/index.js`.
141
148
  4. Hybrids' internal imports use relative paths — they resolve naturally.
149
+ 5. `#prefix/` entries resolve cross-directory app imports without `../`.
150
+ For example, `import '#utils/formatDate.js'` resolves to `/utils/formatDate.js`.
142
151
 
143
152
  ### Rules
144
153
 
package/lib/check.js CHANGED
@@ -6,9 +6,16 @@
6
6
 
7
7
  import { readFileSync, existsSync } from 'node:fs';
8
8
  import { resolve } from 'node:path';
9
- import { checkFileLines, runCmd, countFiles } from './spec-utils.js';
9
+ import { checkFileLines, runCmd, countFiles, checkImports } from './spec-utils.js';
10
10
 
11
- export { checkFileLines, runCmd, countFiles, findFiles, elapsed } from './spec-utils.js';
11
+ export { checkFileLines, runCmd, countFiles, findFiles, elapsed, checkImports } from './spec-utils.js';
12
+
13
+ /** Detect the project's package manager runner (npx, pnpm exec, yarn). */
14
+ function detectRunner(projectDir) {
15
+ if (existsSync(resolve(projectDir, 'pnpm-lock.yaml'))) return 'pnpm exec';
16
+ if (existsSync(resolve(projectDir, 'yarn.lock'))) return 'yarn';
17
+ return 'npx';
18
+ }
12
19
 
13
20
  /** @param {string} src */
14
21
  function parseEnv(src) {
@@ -36,15 +43,24 @@ export function loadConfig(projectDir) {
36
43
  };
37
44
  }
38
45
 
39
- /** Standard check commands used by both CLI and POC. */
40
- export const CMDS = {
41
- lint: 'npx eslint --config .configs/eslint.config.js . --fix',
42
- stylelint: 'npx stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix',
43
- prettier: 'npx prettier --config .configs/.prettierrc --write src scripts',
44
- mdlint: 'npx markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"',
45
- types: 'npx tsc --project .configs/jsconfig.json',
46
- audit: 'npm audit --omit=dev',
47
- };
46
+ /**
47
+ * Build check commands for the detected package manager.
48
+ * @param {string} runner
49
+ */
50
+ function buildCmds(runner) {
51
+ const audit = runner === 'pnpm exec' ? 'pnpm audit --prod' : 'npm audit --omit=dev';
52
+ return {
53
+ lint: `${runner} eslint --config .configs/eslint.config.js . --fix`,
54
+ stylelint: `${runner} stylelint --config .configs/.stylelintrc.json "src/**/*.css" --fix`,
55
+ prettier: `${runner} prettier --config .configs/.prettierrc --write src scripts`,
56
+ mdlint: `${runner} markdownlint-cli2 --config .configs/.markdownlint.jsonc --fix "docs/**/*.md" "*.md"`,
57
+ types: `${runner} tsc --project .configs/jsconfig.json`,
58
+ audit,
59
+ };
60
+ }
61
+
62
+ /** @deprecated Use buildCmds() instead — kept for backward compat. */
63
+ export const CMDS = buildCmds('npx');
48
64
 
49
65
  /**
50
66
  * Run the full spec compliance check (used by clearstack CLI).
@@ -53,6 +69,7 @@ export const CMDS = {
53
69
  */
54
70
  export async function check(projectDir, scope) {
55
71
  const cfg = loadConfig(projectDir);
72
+ const cmds = buildCmds(detectRunner(projectDir));
56
73
 
57
74
  if (scope === 'code') {
58
75
  if (!checkFileLines(projectDir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`))
@@ -73,12 +90,13 @@ export async function check(projectDir, scope) {
73
90
  const results = [
74
91
  checkFileLines(projectDir, cfg.codeExt, cfg.codeMax, cfg.ignore, `Code (max ${cfg.codeMax} lines)`),
75
92
  checkFileLines(projectDir, cfg.docsExt, cfg.docsMax, cfg.ignore, `Docs (max ${cfg.docsMax} lines)`),
76
- runCmd('ESLint', CMDS.lint, projectDir, `${jsFiles} files`),
77
- runCmd('Stylelint', CMDS.stylelint, projectDir, `${cssFiles} files`),
78
- runCmd('Prettier', CMDS.prettier, projectDir, `${jsFiles} files`),
79
- runCmd('Markdown', CMDS.mdlint, projectDir, `${mdFiles} files`),
80
- runCmd('JSDoc types', CMDS.types, projectDir, `${jsFiles} files`),
81
- runCmd('Security audit', CMDS.audit, projectDir),
93
+ checkImports(projectDir, cfg.ignore, 'Import map aliases (no ../ imports)'),
94
+ runCmd('ESLint', cmds.lint, projectDir, `${jsFiles} files`),
95
+ runCmd('Stylelint', cmds.stylelint, projectDir, `${cssFiles} files`),
96
+ runCmd('Prettier', cmds.prettier, projectDir, `${jsFiles} files`),
97
+ runCmd('Markdown', cmds.mdlint, projectDir, `${mdFiles} files`),
98
+ runCmd('JSDoc types', cmds.types, projectDir, `${jsFiles} files`),
99
+ runCmd('Security audit', cmds.audit, projectDir),
82
100
  ];
83
101
 
84
102
  const passed = results.filter(Boolean).length;
@@ -49,6 +49,16 @@ export async function writePackageJson(dest, vars, existing) {
49
49
  typescript: '^6.0.2',
50
50
  };
51
51
 
52
+ const subpathImports = {
53
+ '#store/*': './src/store/*',
54
+ '#utils/*': './src/utils/*',
55
+ '#atoms/*': './src/components/atoms/*',
56
+ '#molecules/*': './src/components/molecules/*',
57
+ '#organisms/*': './src/components/organisms/*',
58
+ '#templates/*': './src/components/templates/*',
59
+ '#pages/*': './src/pages/*',
60
+ };
61
+
52
62
  const pkg = existing
53
63
  ? {
54
64
  ...existing,
@@ -56,6 +66,7 @@ export async function writePackageJson(dest, vars, existing) {
56
66
  description: vars.description,
57
67
  type: 'module',
58
68
  main: 'src/server.js',
69
+ imports: { ...existing.imports, ...subpathImports },
59
70
  scripts: { ...existing.scripts, ...specScripts },
60
71
  dependencies: { ...existing.dependencies, ...specDeps },
61
72
  devDependencies: { ...existing.devDependencies, ...specDevDeps },
@@ -66,6 +77,7 @@ export async function writePackageJson(dest, vars, existing) {
66
77
  type: 'module',
67
78
  description: vars.description,
68
79
  main: 'src/server.js',
80
+ imports: subpathImports,
69
81
  scripts: specScripts,
70
82
  keywords: ['hybrids', 'web-components', 'no-build'],
71
83
  license: 'MIT',
package/lib/spec-utils.js CHANGED
@@ -79,6 +79,38 @@ export function checkFileLines(root, extensions, max, ignoreDirs, label) {
79
79
  return false;
80
80
  }
81
81
 
82
+ /**
83
+ * Check for relative parent imports (`../`) in browser-facing JS files.
84
+ * Same-directory (`./`) and server/test files are allowed.
85
+ * @param {string} root
86
+ * @param {string[]} ignoreDirs
87
+ * @param {string} label
88
+ * @returns {boolean}
89
+ */
90
+ export function checkImports(root, ignoreDirs, label) {
91
+ const start = performance.now();
92
+ const files = findFiles(root, ['.js'], ignoreDirs, root)
93
+ .filter((f) => f.startsWith('src/') && !f.includes('vendor/') && !f.includes('api/') && !f.endsWith('.test.js') && !f.endsWith('server.js'));
94
+ const violations = [];
95
+ const importRe = /(?:^|\n)\s*import\s.*?from\s+['"](\.\.[^'"]*)['"]|(?:^|\n)\s*import\s+['"](\.\.[^'"]*)['"]|(?:^|\n)\s*export\s.*?from\s+['"](\.\.[^'"]*)['"/]/g;
96
+ for (const file of files) {
97
+ const src = readFileSync(resolve(root, file), 'utf-8');
98
+ let m;
99
+ while ((m = importRe.exec(src)) !== null) {
100
+ const spec = m[1] || m[2] || m[3];
101
+ violations.push({ file, spec });
102
+ }
103
+ }
104
+ const time = elapsed(start);
105
+ if (violations.length === 0) {
106
+ console.log(` ✅ ${label} (${files.length} files, ${time})`);
107
+ return true;
108
+ }
109
+ console.log(` ❌ ${label} — ${violations.length} violation(s):`);
110
+ for (const v of violations) console.log(` ${v.file}: import '${v.spec}' → use #prefix/ alias`);
111
+ return false;
112
+ }
113
+
82
114
  /**
83
115
  * Run a shell command, report pass/fail with timing.
84
116
  * @param {string} label
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -31,6 +31,15 @@
31
31
  "publishConfig": {
32
32
  "access": "public"
33
33
  },
34
+ "imports": {
35
+ "#store/*": "./src/store/*",
36
+ "#utils/*": "./src/utils/*",
37
+ "#atoms/*": "./src/components/atoms/*",
38
+ "#molecules/*": "./src/components/molecules/*",
39
+ "#organisms/*": "./src/components/organisms/*",
40
+ "#templates/*": "./src/components/templates/*",
41
+ "#pages/*": "./src/pages/*"
42
+ },
34
43
  "keywords": [
35
44
  "web-components",
36
45
  "no-build",
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { html, define, router } from 'hybrids';
7
- import HomeView from '../pages/home/home-view.js';
7
+ import HomeView from '#pages/home/home-view.js';
8
8
 
9
9
  export default define({
10
10
  tag: 'app-router',
@@ -10,7 +10,14 @@
10
10
  "skipLibCheck": true,
11
11
  "types": ["node"],
12
12
  "paths": {
13
- "hybrids": ["../node_modules/hybrids/types/index.d.ts"]
13
+ "hybrids": ["../node_modules/hybrids/types/index.d.ts"],
14
+ "#store/*": ["../src/store/*"],
15
+ "#utils/*": ["../src/utils/*"],
16
+ "#atoms/*": ["../src/components/atoms/*"],
17
+ "#molecules/*": ["../src/components/molecules/*"],
18
+ "#organisms/*": ["../src/components/organisms/*"],
19
+ "#templates/*": ["../src/components/templates/*"],
20
+ "#pages/*": ["../src/pages/*"]
14
21
  }
15
22
  },
16
23
  "include": ["../src/**/*.js", "../scripts/**/*.js"],
@@ -109,6 +109,50 @@ render: ({ items }) => html`
109
109
 
110
110
  ---
111
111
 
112
+ ## Import Map Aliases
113
+
114
+ All cross-directory imports in browser-facing code use `#prefix/` aliases
115
+ defined in the import map (`src/index.html`). This eliminates fragile
116
+ relative paths like `../../../utils/foo.js`.
117
+
118
+ ### Available Prefixes
119
+
120
+ | Prefix | Resolves to |
121
+ | --------------- | ------------------------------ |
122
+ | `#store/` | `/store/` |
123
+ | `#utils/` | `/utils/` |
124
+ | `#atoms/` | `/components/atoms/` |
125
+ | `#molecules/` | `/components/molecules/` |
126
+ | `#organisms/` | `/components/organisms/` |
127
+ | `#templates/` | `/components/templates/` |
128
+ | `#pages/` | `/pages/` |
129
+
130
+ ### Rules
131
+
132
+ - **No `../` imports.** The spec check (`npm run spec`) flags any `../`
133
+ in browser-facing JS. Use a `#prefix/` alias instead.
134
+ - **Same-directory `./` is fine.** `index.js` re-exports and co-located
135
+ files use `./` naturally.
136
+ - **Server/API files are exempt.** `src/api/` and `src/server.js` run in
137
+ Node, not the browser — they use `./` relative imports.
138
+ - **Test files are exempt.** `*.test.js` files may use relative paths.
139
+ - **jsconfig.json mirrors the import map.** The `paths` entries in
140
+ `.configs/jsconfig.json` let `tsc --checkJs` resolve `#prefix/` imports.
141
+
142
+ ### Examples
143
+
144
+ ```javascript
145
+ // ✅ Good — clear, refactor-safe
146
+ import AppState from '#store/AppState.js';
147
+ import { formatDate } from '#utils/formatDate.js';
148
+ import '#atoms/app-badge/app-badge.js';
149
+
150
+ // ❌ Bad — fragile, hard to read
151
+ import AppState from '../../../store/AppState.js';
152
+ ```
153
+
154
+ ---
155
+
112
156
  ## Error Handling
113
157
 
114
158
  Errors are handled **at the boundary where they are actionable.** Each layer
@@ -193,8 +237,8 @@ state: store(AppState),
193
237
  // In src/utils/filterActive.js
194
238
  export const filterActive = (items) => items.filter(i => i.active);
195
239
 
196
- // In component
197
- import { filterActive } from '../../utils/filterActive.js';
240
+ // In component — use import map alias, never ../
241
+ import { filterActive } from '#utils/filterActive.js';
198
242
  render: ({ items }) => html`<ul>${filterActive(items).map(...)}</ul>`,
199
243
  ```
200
244
 
@@ -121,7 +121,14 @@ declared in `src/index.html`.
121
121
  <script type="importmap">
122
122
  {
123
123
  "imports": {
124
- "hybrids": "/vendor/hybrids/index.js"
124
+ "hybrids": "/vendor/hybrids/index.js",
125
+ "#store/": "/store/",
126
+ "#utils/": "/utils/",
127
+ "#atoms/": "/components/atoms/",
128
+ "#molecules/": "/components/molecules/",
129
+ "#organisms/": "/components/organisms/",
130
+ "#templates/": "/components/templates/",
131
+ "#pages/": "/pages/"
125
132
  }
126
133
  }
127
134
  </script>
@@ -139,6 +146,8 @@ declared in `src/index.html`.
139
146
  2. Import map resolves `'hybrids'` → `/vendor/hybrids/index.js`.
140
147
  3. Server serves the file from `src/vendor/hybrids/index.js`.
141
148
  4. Hybrids' internal imports use relative paths — they resolve naturally.
149
+ 5. `#prefix/` entries resolve cross-directory app imports without `../`.
150
+ For example, `import '#utils/formatDate.js'` resolves to `/utils/formatDate.js`.
142
151
 
143
152
  ### Rules
144
153
 
@@ -5,11 +5,11 @@
5
5
  */
6
6
 
7
7
  import { html, define, store } from 'hybrids';
8
- import AppState from '../../../store/AppState.js';
8
+ import AppState from '#store/AppState.js';
9
9
 
10
10
  /**
11
11
  * @typedef {Object} ThemeToggleHost
12
- * @property {import('../../../store/AppState.js').AppState} state
12
+ * @property {import('#store/AppState.js').AppState} state
13
13
  */
14
14
 
15
15
  /**
@@ -14,7 +14,14 @@
14
14
  <script type="importmap">
15
15
  {
16
16
  "imports": {
17
- "hybrids": "/vendor/hybrids/index.js"
17
+ "hybrids": "/vendor/hybrids/index.js",
18
+ "#store/": "/store/",
19
+ "#utils/": "/utils/",
20
+ "#atoms/": "/components/atoms/",
21
+ "#molecules/": "/components/molecules/",
22
+ "#organisms/": "/components/organisms/",
23
+ "#templates/": "/components/templates/",
24
+ "#pages/": "/pages/"
18
25
  }
19
26
  }
20
27
  </script>
@@ -4,12 +4,12 @@
4
4
  */
5
5
 
6
6
  import { html, define, store } from 'hybrids';
7
- import AppState from '../../store/AppState.js';
8
- import '../../components/atoms/app-button/app-button.js';
7
+ import AppState from '#store/AppState.js';
8
+ import '#atoms/app-button/app-button.js';
9
9
 
10
10
  /**
11
11
  * @typedef {Object} HomeViewHost
12
- * @property {import('../../store/AppState.js').AppState} state
12
+ * @property {import('#store/AppState.js').AppState} state
13
13
  */
14
14
 
15
15
  /** @param {HomeViewHost & HTMLElement} host */
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { html, define, router } from 'hybrids';
7
- import HomeView from '../pages/home/home-view.js';
7
+ import HomeView from '#pages/home/home-view.js';
8
8
 
9
9
  export default define({
10
10
  tag: 'app-router',