neutrinos-cli 2.0.0-beta.3 → 2.0.0-beta.4

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.
Files changed (32) hide show
  1. package/dist/src/bin/cli.js +25 -0
  2. package/dist/src/commands/dev.js +12 -7
  3. package/dist/src/commands/doctor.js +1 -1
  4. package/dist/src/commands/new-workspace.js +9 -8
  5. package/dist/src/commands/test.js +27 -0
  6. package/dist/src/utils/doctor-checks/auth-state.js +21 -0
  7. package/dist/src/utils/doctor-checks/component-entry-file.js +40 -0
  8. package/dist/src/utils/doctor-checks/get-packages-safe.js +16 -0
  9. package/dist/src/utils/doctor-checks/index.js +26 -0
  10. package/dist/src/utils/doctor-checks/lock-file-sync.js +38 -0
  11. package/dist/src/utils/doctor-checks/node-modules.js +17 -0
  12. package/dist/src/utils/doctor-checks/node-version.js +10 -0
  13. package/dist/src/utils/doctor-checks/package-alpha-block.js +62 -0
  14. package/dist/src/utils/doctor-checks/plugin-json.js +56 -0
  15. package/dist/src/utils/doctor-checks/plugins-server.js +21 -0
  16. package/dist/src/utils/doctor-checks/root-package-json.js +51 -0
  17. package/dist/src/utils/doctor-checks/storybook.js +26 -0
  18. package/dist/src/utils/doctor-checks/tsconfig.js +25 -0
  19. package/dist/src/utils/doctor-checks/vitest-config.js +16 -0
  20. package/dist/src/utils/doctor-checks.js +42 -2
  21. package/dist/src/utils/generate-component.js +3 -2
  22. package/dist/src/utils/local-cli.js +23 -0
  23. package/package.json +1 -1
  24. package/templates/component/.stories.ts.hbs +53 -0
  25. package/templates/project/.storybook/decorators/event-logger.ts +153 -0
  26. package/templates/project/.storybook/main.ts +19 -0
  27. package/templates/project/.storybook/preview.ts +12 -0
  28. package/templates/project/tsconfig.json +1 -1
  29. package/templates/project/vitest.config.ts +24 -0
  30. package/templates/component/.spec.ts.hbs +0 -15
  31. package/templates/project/index.html +0 -24
  32. package/templates/project/index.ts +0 -86
@@ -4,6 +4,21 @@ if (major < 22) {
4
4
  console.error('neutrinos requires Node.js 22+. Current: ' + process.version);
5
5
  process.exit(1);
6
6
  }
7
+ // Delegate to workspace-local CLI binary if it exists and differs from this one.
8
+ // This ensures the workspace's pinned CLI version is used, not the global install.
9
+ import { resolveLocalCli } from '../utils/local-cli.js';
10
+ const localScript = resolveLocalCli(process.cwd(), realpathSync(fileURLToPath(import.meta.url)));
11
+ if (localScript) {
12
+ const child = spawn(process.execPath, [localScript, ...process.argv.slice(2)], {
13
+ stdio: 'inherit',
14
+ });
15
+ process.on('SIGINT', () => child.kill('SIGINT'));
16
+ process.on('SIGTERM', () => child.kill('SIGTERM'));
17
+ const code = await new Promise((resolve) => {
18
+ child.on('close', (c) => resolve(c ?? 1));
19
+ });
20
+ process.exit(code);
21
+ }
7
22
  import dotenv from 'dotenv';
8
23
  import { PACKAGE_ROOT } from '../utils/path-utils.js';
9
24
  dotenv.config({
@@ -14,6 +29,7 @@ import { Argument, Command, Option } from 'commander';
14
29
  import EventEmitter from 'node:events';
15
30
  import { cwd, env, exit } from 'node:process';
16
31
  import { readFileSync, realpathSync } from 'node:fs';
32
+ import { spawn } from 'node:child_process';
17
33
  import { fileURLToPath } from 'node:url';
18
34
  import open from 'open';
19
35
  import { join, resolve } from 'node:path';
@@ -26,6 +42,7 @@ import { generate } from '../commands/generate.js';
26
42
  import { createWorkspace } from '../commands/new-workspace.js';
27
43
  import { publish } from '../commands/publish.js';
28
44
  import { startPluginsServer } from '../commands/serve.js';
45
+ import { runTests } from '../commands/test.js';
29
46
  import { completion } from '../commands/completion.js';
30
47
  import { doctor } from '../commands/doctor.js';
31
48
  import { getPackages } from '../utils/get-packages.js';
@@ -179,6 +196,14 @@ export const createProgram = () => {
179
196
  .action((options) => {
180
197
  servePlugin(cwd(), Number(options.port) || 6969);
181
198
  });
199
+ program
200
+ .command('test')
201
+ .alias('t')
202
+ .option('-w, --watch', 'Run in watch mode')
203
+ .description('run story-based tests')
204
+ .action((options) => {
205
+ runTests(cwd(), options);
206
+ });
182
207
  program
183
208
  .command('serve')
184
209
  .option('-p, --port <port>', 'Port number for serving the plugins', '3000')
@@ -1,10 +1,15 @@
1
- import { createServer } from 'vite';
1
+ import { spawn } from 'node:child_process';
2
2
  export const servePlugin = async (wsPath, port) => {
3
- const server = await createServer({
4
- root: wsPath,
5
- server: { port },
3
+ const child = spawn('npx', ['storybook', 'dev', '-p', String(port)], {
4
+ cwd: wsPath,
5
+ stdio: 'inherit',
6
+ });
7
+ await new Promise((resolve, reject) => {
8
+ child.on('close', (code) => {
9
+ if (code === 0)
10
+ resolve();
11
+ else
12
+ reject(new Error(`Storybook exited with code ${code}`));
13
+ });
6
14
  });
7
- await server.listen();
8
- server.printUrls();
9
- server.bindCLIShortcuts({ print: true });
10
15
  };
@@ -1,6 +1,6 @@
1
1
  import { bold, greenBright, red, yellowBright } from 'colorette';
2
2
  import { log as _log } from 'node:console';
3
- import { checks } from '../utils/doctor-checks.js';
3
+ import { checks } from '../utils/doctor-checks/index.js';
4
4
  const SYMBOLS = {
5
5
  pass: greenBright('✔'),
6
6
  warn: yellowBright('⚠'),
@@ -75,16 +75,19 @@ const cleanUp = (dir) => {
75
75
  done('Cleaned up workspace');
76
76
  };
77
77
  const initializePackages = async (dir) => {
78
- inprogress('Adding default npm scripts to workspace...');
78
+ inprogress('Installing default packages in workspace...');
79
79
  const pkgJson = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
80
- const scripts = {
81
- build: 'neutrinos build',
82
- start: 'neutrinos start',
83
- serve: 'neutrinos serve',
84
- };
80
+ pkgJson.scripts = {};
85
81
  pkgJson.devDependencies = {
82
+ 'neutrinos-cli': '^2.0.0',
86
83
  lit: '^3.3.2',
87
84
  typescript: '^5.9.3',
85
+ storybook: '^10.3.1',
86
+ '@storybook/web-components-vite': '^10.3.1',
87
+ '@storybook/addon-vitest': '^10.3.1',
88
+ vitest: '^4.0.0',
89
+ '@vitest/browser': '^4.0.0',
90
+ '@vitest/browser-playwright': '^4.0.0',
88
91
  };
89
92
  pkgJson.dependencies = {
90
93
  express: '^5.2.1',
@@ -92,13 +95,11 @@ const initializePackages = async (dir) => {
92
95
  };
93
96
  pkgJson.workspaces = ['packages/*'];
94
97
  pkgJson.type = 'module';
95
- Object.assign(pkgJson.scripts, {}, scripts);
96
98
  writeFileSync(join(dir, 'package.json'), await prettify(pkgJson, 'json'));
97
99
  execSync('npm install', {
98
100
  cwd: dir,
99
101
  });
100
102
  done(`Installed default packages in workspace: ${bold(dir)}`);
101
- done(`Added default npm scripts to workspace: ${bold(dir)}`);
102
103
  };
103
104
  const initializeStaticServer = (dir) => {
104
105
  cpSync(pluginServerTemplatesPath(), join(dir, 'plugins-server'), {
@@ -0,0 +1,27 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { failed } from '../utils/logger.js';
5
+ export const runTests = async (wsPath, options) => {
6
+ const vitestConfig = join(wsPath, 'vitest.config.ts');
7
+ if (!existsSync(vitestConfig)) {
8
+ failed('vitest.config.ts not found in workspace. Run "neutrinos doctor --fix" or create it manually.');
9
+ process.exit(1);
10
+ }
11
+ const args = ['vitest'];
12
+ if (!options.watch) {
13
+ args.push('--run');
14
+ }
15
+ const child = spawn('npx', args, {
16
+ cwd: wsPath,
17
+ stdio: 'inherit',
18
+ });
19
+ await new Promise((resolve, reject) => {
20
+ child.on('close', (code) => {
21
+ if (code === 0)
22
+ resolve();
23
+ else
24
+ reject(new Error(`Vitest exited with code ${code}`));
25
+ });
26
+ });
27
+ };
@@ -0,0 +1,21 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { authConfigJson } from '../path-utils.js';
3
+ export const authStateCheck = {
4
+ name: 'Auth state',
5
+ run() {
6
+ const authPath = authConfigJson();
7
+ if (!existsSync(authPath)) {
8
+ return [{ name: 'Auth state', status: 'fail', message: `${authPath} not found. Run "neutrinos auth login"`, fixable: false }];
9
+ }
10
+ try {
11
+ const tokenSet = JSON.parse(readFileSync(authPath, 'utf-8'));
12
+ if (tokenSet.expires_at && tokenSet.expires_at < Date.now() / 1000) {
13
+ return [{ name: 'Auth state', status: 'warn', message: 'Token expired. Run "neutrinos auth login"', fixable: false }];
14
+ }
15
+ }
16
+ catch {
17
+ return [{ name: 'Auth state', status: 'warn', message: 'Could not parse auth.json', fixable: false }];
18
+ }
19
+ return [{ name: 'Auth state', status: 'pass', message: 'Token present and valid', fixable: false }];
20
+ },
21
+ };
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { getGeneratedComponentName } from '../path-utils.js';
4
+ import { getPackagesSafe } from './get-packages-safe.js';
5
+ export const componentEntryFileCheck = {
6
+ name: 'Component entry file',
7
+ run(wsPath) {
8
+ const packages = getPackagesSafe(wsPath);
9
+ if (packages === null) {
10
+ return [{ name: 'Component entry file', status: 'fail', message: 'Could not discover packages', fixable: false }];
11
+ }
12
+ const results = [];
13
+ for (const pkgPath of packages) {
14
+ const pkgJsonPath = join(pkgPath, 'package.json');
15
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
16
+ if (!pkgJson.alpha?.component)
17
+ continue;
18
+ const pkgName = pkgJson.name || basename(pkgPath);
19
+ const entryName = getGeneratedComponentName(basename(pkgPath));
20
+ const entryFile = join(pkgPath, `${entryName}.ts`);
21
+ if (!existsSync(entryFile)) {
22
+ results.push({
23
+ name: `Package "${pkgName}" entry file`,
24
+ status: 'fail',
25
+ message: `${entryName}.ts is missing`,
26
+ fixable: false,
27
+ });
28
+ }
29
+ else {
30
+ results.push({
31
+ name: `Package "${pkgName}" entry file`,
32
+ status: 'pass',
33
+ message: `${entryName}.ts exists`,
34
+ fixable: false,
35
+ });
36
+ }
37
+ }
38
+ return results;
39
+ },
40
+ };
@@ -0,0 +1,16 @@
1
+ import { existsSync, globSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export function getPackagesSafe(wsPath) {
4
+ try {
5
+ const packageJsonContent = readFileSync(join(wsPath, 'package.json'), 'utf-8');
6
+ const parsed = JSON.parse(packageJsonContent);
7
+ const packagesDir = parsed.workspaces ?? [];
8
+ const packageNames = globSync(packagesDir, { cwd: wsPath });
9
+ return packageNames
10
+ .map((name) => join(wsPath, name))
11
+ .filter((p) => existsSync(join(p, 'package.json')));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
@@ -0,0 +1,26 @@
1
+ import { nodeModulesCheck } from './node-modules.js';
2
+ import { pluginJsonCheck } from './plugin-json.js';
3
+ import { rootPackageJsonCheck } from './root-package-json.js';
4
+ import { tsconfigCheck } from './tsconfig.js';
5
+ import { pluginsServerCheck } from './plugins-server.js';
6
+ import { storybookCheck } from './storybook.js';
7
+ import { vitestConfigCheck } from './vitest-config.js';
8
+ import { packageAlphaBlockCheck } from './package-alpha-block.js';
9
+ import { componentEntryFileCheck } from './component-entry-file.js';
10
+ import { nodeVersionCheck } from './node-version.js';
11
+ import { authStateCheck } from './auth-state.js';
12
+ import { lockFileSyncCheck } from './lock-file-sync.js';
13
+ export const checks = [
14
+ nodeModulesCheck,
15
+ pluginJsonCheck,
16
+ rootPackageJsonCheck,
17
+ tsconfigCheck,
18
+ pluginsServerCheck,
19
+ storybookCheck,
20
+ vitestConfigCheck,
21
+ packageAlphaBlockCheck,
22
+ componentEntryFileCheck,
23
+ nodeVersionCheck,
24
+ authStateCheck,
25
+ lockFileSyncCheck,
26
+ ];
@@ -0,0 +1,38 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const lockFileSyncCheck = {
5
+ name: 'Lock file sync',
6
+ run(wsPath) {
7
+ const packageJsonPath = join(wsPath, 'package.json');
8
+ if (!existsSync(packageJsonPath)) {
9
+ return [{ name: 'Lock file sync', status: 'fail', message: 'No package.json to compare against', fixable: false }];
10
+ }
11
+ const pnpmLock = join(wsPath, 'pnpm-lock.yaml');
12
+ const npmLock = join(wsPath, 'package-lock.json');
13
+ let lockPath = null;
14
+ let manager = 'npm';
15
+ if (existsSync(pnpmLock)) {
16
+ lockPath = pnpmLock;
17
+ manager = 'pnpm';
18
+ }
19
+ else if (existsSync(npmLock)) {
20
+ lockPath = npmLock;
21
+ manager = 'npm';
22
+ }
23
+ if (!lockPath) {
24
+ return [{ name: 'Lock file sync', status: 'fail', message: 'No lock file found. Run "npm install" or "pnpm install"', fixable: true }];
25
+ }
26
+ const lockMtime = statSync(lockPath).mtimeMs;
27
+ const pkgMtime = statSync(packageJsonPath).mtimeMs;
28
+ if (pkgMtime > lockMtime) {
29
+ return [{ name: 'Lock file sync', status: 'warn', message: `package.json is newer than lock file. Run "${manager} install"`, fixable: true }];
30
+ }
31
+ return [{ name: 'Lock file sync', status: 'pass', message: 'Lock file is up to date', fixable: false }];
32
+ },
33
+ fix(wsPath) {
34
+ const pnpmLock = join(wsPath, 'pnpm-lock.yaml');
35
+ const manager = existsSync(pnpmLock) ? 'pnpm' : 'npm';
36
+ execSync(`${manager} install`, { cwd: wsPath, stdio: 'inherit' });
37
+ },
38
+ };
@@ -0,0 +1,17 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const nodeModulesCheck = {
5
+ name: 'node_modules',
6
+ run(wsPath) {
7
+ if (!existsSync(join(wsPath, 'node_modules'))) {
8
+ return [{ name: 'node_modules', status: 'fail', message: 'Not installed. Run "npm install" or "pnpm install"', fixable: true }];
9
+ }
10
+ return [{ name: 'node_modules', status: 'pass', message: 'Dependencies installed', fixable: false }];
11
+ },
12
+ fix(wsPath) {
13
+ const pnpmLock = join(wsPath, 'pnpm-lock.yaml');
14
+ const manager = existsSync(pnpmLock) ? 'pnpm' : 'npm';
15
+ execSync(`${manager} install`, { cwd: wsPath, stdio: 'inherit' });
16
+ },
17
+ };
@@ -0,0 +1,10 @@
1
+ export const nodeVersionCheck = {
2
+ name: 'Node version',
3
+ run() {
4
+ const [major] = process.versions.node.split('.').map(Number);
5
+ if (major < 22) {
6
+ return [{ name: 'Node version', status: 'fail', message: `v${process.versions.node} does not meet requirement (>=22). Install Node.js 22+`, fixable: false }];
7
+ }
8
+ return [{ name: 'Node version', status: 'pass', message: `v${process.versions.node} meets requirement (>=22)`, fixable: false }];
9
+ },
10
+ };
@@ -0,0 +1,62 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { getPackagesSafe } from './get-packages-safe.js';
4
+ export const packageAlphaBlockCheck = {
5
+ name: 'Package alpha block',
6
+ run(wsPath) {
7
+ const packages = getPackagesSafe(wsPath);
8
+ if (packages === null) {
9
+ return [{ name: 'Package alpha block', status: 'fail', message: 'Could not discover packages (missing package.json or workspaces)', fixable: false }];
10
+ }
11
+ if (packages.length === 0) {
12
+ return [{ name: 'Package alpha block', status: 'pass', message: 'No packages found (nothing to check)', fixable: false }];
13
+ }
14
+ const results = [];
15
+ for (const pkgPath of packages) {
16
+ const pkgJsonPath = join(pkgPath, 'package.json');
17
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
18
+ const pkgName = pkgJson.name || basename(pkgPath);
19
+ if (!pkgJson.alpha) {
20
+ results.push({
21
+ name: `Package "${pkgName}" alpha block`,
22
+ status: 'fail',
23
+ message: 'No alpha block found. Add alpha.component or alpha.module',
24
+ fixable: true,
25
+ });
26
+ }
27
+ else if (!pkgJson.alpha.component && !pkgJson.alpha.module) {
28
+ results.push({
29
+ name: `Package "${pkgName}" alpha block`,
30
+ status: 'warn',
31
+ message: 'alpha block exists but neither component nor module is true',
32
+ fixable: true,
33
+ });
34
+ }
35
+ else {
36
+ const type = pkgJson.alpha.component ? 'alpha.component' : 'alpha.module';
37
+ results.push({
38
+ name: `Package "${pkgName}" alpha block`,
39
+ status: 'pass',
40
+ message: `${type} is set`,
41
+ fixable: false,
42
+ });
43
+ }
44
+ }
45
+ return results;
46
+ },
47
+ fix(wsPath) {
48
+ const packages = getPackagesSafe(wsPath);
49
+ if (!packages)
50
+ return;
51
+ for (const pkgPath of packages) {
52
+ const pkgJsonPath = join(pkgPath, 'package.json');
53
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
54
+ const alpha = (pkgJson['alpha'] ?? {});
55
+ if (!alpha['component'] && !alpha['module']) {
56
+ alpha['component'] = true;
57
+ }
58
+ pkgJson['alpha'] = alpha;
59
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 4) + '\n');
60
+ }
61
+ },
62
+ };
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { pluginJsonPath } from '../path-utils.js';
4
+ export const pluginJsonCheck = {
5
+ name: 'plugin.json',
6
+ run(wsPath) {
7
+ const path = pluginJsonPath(wsPath);
8
+ if (!existsSync(path)) {
9
+ return [{ name: 'plugin.json', status: 'fail', message: 'File is missing', fixable: true }];
10
+ }
11
+ let json;
12
+ try {
13
+ json = JSON.parse(readFileSync(path, 'utf-8'));
14
+ }
15
+ catch {
16
+ return [{ name: 'plugin.json', status: 'fail', message: 'File is not valid JSON', fixable: false }];
17
+ }
18
+ const missing = [];
19
+ if (!json.name)
20
+ missing.push('name');
21
+ if (!json.components?.selectorPrefix)
22
+ missing.push('components.selectorPrefix');
23
+ if (!json.modules?.idPrefix)
24
+ missing.push('modules.idPrefix');
25
+ if (missing.length > 0) {
26
+ return [{ name: 'plugin.json', status: 'fail', message: `Missing required fields: ${missing.join(', ')}`, fixable: true }];
27
+ }
28
+ return [{ name: 'plugin.json', status: 'pass', message: 'All required fields present', fixable: false }];
29
+ },
30
+ fix(wsPath) {
31
+ const path = pluginJsonPath(wsPath);
32
+ const defaults = {
33
+ name: basename(wsPath),
34
+ components: { selectorPrefix: 'comp' },
35
+ modules: { idPrefix: 'mod' },
36
+ };
37
+ if (!existsSync(path)) {
38
+ writeFileSync(path, JSON.stringify(defaults, null, 4) + '\n');
39
+ return;
40
+ }
41
+ let existing = {};
42
+ try {
43
+ existing = JSON.parse(readFileSync(path, 'utf-8'));
44
+ }
45
+ catch {
46
+ writeFileSync(path, JSON.stringify(defaults, null, 4) + '\n');
47
+ return;
48
+ }
49
+ existing.name ??= defaults.name;
50
+ existing.components ??= {};
51
+ existing.components.selectorPrefix ??= defaults.components.selectorPrefix;
52
+ existing.modules ??= {};
53
+ existing.modules.idPrefix ??= defaults.modules.idPrefix;
54
+ writeFileSync(path, JSON.stringify(existing, null, 4) + '\n');
55
+ },
56
+ };
@@ -0,0 +1,21 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { pluginServerRoot, pluginServerTemplatesPath } from '../path-utils.js';
4
+ export const pluginsServerCheck = {
5
+ name: 'plugins-server/',
6
+ run(wsPath) {
7
+ const serverDir = pluginServerRoot(wsPath);
8
+ if (!existsSync(serverDir)) {
9
+ return [{ name: 'plugins-server/', status: 'fail', message: 'Directory is missing', fixable: true }];
10
+ }
11
+ if (!existsSync(join(serverDir, 'index.js'))) {
12
+ return [{ name: 'plugins-server/', status: 'fail', message: 'Directory exists but index.js is missing', fixable: true }];
13
+ }
14
+ return [{ name: 'plugins-server/', status: 'pass', message: 'Directory exists with index.js', fixable: false }];
15
+ },
16
+ fix(wsPath) {
17
+ const serverDir = pluginServerRoot(wsPath);
18
+ const templateDir = pluginServerTemplatesPath();
19
+ cpSync(templateDir, serverDir, { recursive: true });
20
+ },
21
+ };
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const rootPackageJsonCheck = {
4
+ name: 'Root package.json',
5
+ run(wsPath) {
6
+ const path = join(wsPath, 'package.json');
7
+ if (!existsSync(path)) {
8
+ return [{ name: 'Root package.json', status: 'fail', message: 'File is missing', fixable: false }];
9
+ }
10
+ let json;
11
+ try {
12
+ json = JSON.parse(readFileSync(path, 'utf-8'));
13
+ }
14
+ catch {
15
+ return [{ name: 'Root package.json', status: 'fail', message: 'File is not valid JSON', fixable: false }];
16
+ }
17
+ const issues = [];
18
+ if (!json['workspaces'])
19
+ issues.push('workspaces');
20
+ if (json['type'] !== 'module')
21
+ issues.push('type: "module"');
22
+ const deps = (json['dependencies'] ?? {});
23
+ if (!deps['@jatahworx/alpha-annotations-lib'])
24
+ issues.push('dependency @jatahworx/alpha-annotations-lib');
25
+ const devDeps = (json['devDependencies'] ?? {});
26
+ if (!devDeps['lit'])
27
+ issues.push('devDependency lit');
28
+ if (!devDeps['typescript'])
29
+ issues.push('devDependency typescript');
30
+ if (issues.length > 0) {
31
+ const fixable = issues.includes('workspaces') || issues.includes('type: "module"');
32
+ return [{ name: 'Root package.json', status: 'fail', message: `Missing: ${issues.join(', ')}`, fixable }];
33
+ }
34
+ return [{ name: 'Root package.json', status: 'pass', message: 'workspaces, dependencies, and type configured', fixable: false }];
35
+ },
36
+ fix(wsPath) {
37
+ const path = join(wsPath, 'package.json');
38
+ if (!existsSync(path))
39
+ return;
40
+ let json;
41
+ try {
42
+ json = JSON.parse(readFileSync(path, 'utf-8'));
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ json['workspaces'] ??= ['packages/*'];
48
+ json['type'] ??= 'module';
49
+ writeFileSync(path, JSON.stringify(json, null, 4) + '\n');
50
+ },
51
+ };
@@ -0,0 +1,26 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { templatesPath } from '../path-utils.js';
4
+ export const storybookCheck = {
5
+ name: '.storybook/',
6
+ run(wsPath) {
7
+ const storybookDir = join(wsPath, '.storybook');
8
+ if (!existsSync(storybookDir)) {
9
+ return [{ name: '.storybook/', status: 'fail', message: 'Directory is missing', fixable: true }];
10
+ }
11
+ const missing = [];
12
+ if (!existsSync(join(storybookDir, 'main.ts')))
13
+ missing.push('main.ts');
14
+ if (!existsSync(join(storybookDir, 'preview.ts')))
15
+ missing.push('preview.ts');
16
+ if (missing.length > 0) {
17
+ return [{ name: '.storybook/', status: 'fail', message: `Missing: ${missing.join(', ')}`, fixable: true }];
18
+ }
19
+ return [{ name: '.storybook/', status: 'pass', message: 'main.ts and preview.ts present', fixable: false }];
20
+ },
21
+ fix(wsPath) {
22
+ const templateDir = join(templatesPath(), 'project', '.storybook');
23
+ const storybookDir = join(wsPath, '.storybook');
24
+ cpSync(templateDir, storybookDir, { recursive: true });
25
+ },
26
+ };
@@ -0,0 +1,25 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ export const tsconfigCheck = {
5
+ name: 'tsconfig.json',
6
+ run(wsPath) {
7
+ const path = join(wsPath, 'tsconfig.json');
8
+ if (!existsSync(path)) {
9
+ return [{ name: 'tsconfig.json', status: 'fail', message: 'File is missing. Create a tsconfig.json', fixable: false }];
10
+ }
11
+ try {
12
+ execSync('npx tsc --showConfig', { cwd: wsPath, stdio: 'pipe', timeout: 15_000 });
13
+ }
14
+ catch (err) {
15
+ const stderr = err instanceof Error && 'stderr' in err
16
+ ? String(err.stderr).trim()
17
+ : '';
18
+ if (stderr.includes('ENOENT') || stderr.includes('not found') || !stderr) {
19
+ return [{ name: 'tsconfig.json', status: 'warn', message: 'File exists but could not run tsc to validate. Ensure typescript is installed', fixable: false }];
20
+ }
21
+ return [{ name: 'tsconfig.json', status: 'fail', message: `Invalid config: ${stderr}`, fixable: false }];
22
+ }
23
+ return [{ name: 'tsconfig.json', status: 'pass', message: 'Valid configuration', fixable: false }];
24
+ },
25
+ };
@@ -0,0 +1,16 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { templatesPath } from '../path-utils.js';
4
+ export const vitestConfigCheck = {
5
+ name: 'vitest.config.ts',
6
+ run(wsPath) {
7
+ if (!existsSync(join(wsPath, 'vitest.config.ts'))) {
8
+ return [{ name: 'vitest.config.ts', status: 'fail', message: 'File is missing', fixable: true }];
9
+ }
10
+ return [{ name: 'vitest.config.ts', status: 'pass', message: 'File exists', fixable: false }];
11
+ },
12
+ fix(wsPath) {
13
+ const templateFile = join(templatesPath(), 'project', 'vitest.config.ts');
14
+ cpSync(templateFile, join(wsPath, 'vitest.config.ts'));
15
+ },
16
+ };
@@ -1,7 +1,7 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { cpSync, existsSync, globSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
- import { authConfigJson, pluginJsonPath, pluginServerRoot, pluginServerTemplatesPath } from './path-utils.js';
4
+ import { authConfigJson, pluginJsonPath, pluginServerRoot, pluginServerTemplatesPath, templatesPath } from './path-utils.js';
5
5
  import { getGeneratedComponentName } from './path-utils.js';
6
6
  // ── Check 0: node_modules ──────────────────────────────────────────────
7
7
  const nodeModulesCheck = {
@@ -164,7 +164,45 @@ const pluginsServerCheck = {
164
164
  cpSync(templateDir, serverDir, { recursive: true });
165
165
  },
166
166
  };
167
- // ── Check 5: Package alpha block ───────────────────────────────────────
167
+ // ── Check 5: .storybook/ ───────────────────────────────────────────────
168
+ const storybookCheck = {
169
+ name: '.storybook/',
170
+ run(wsPath) {
171
+ const storybookDir = join(wsPath, '.storybook');
172
+ if (!existsSync(storybookDir)) {
173
+ return [{ name: '.storybook/', status: 'fail', message: 'Directory is missing', fixable: true }];
174
+ }
175
+ const missing = [];
176
+ if (!existsSync(join(storybookDir, 'main.ts')))
177
+ missing.push('main.ts');
178
+ if (!existsSync(join(storybookDir, 'preview.ts')))
179
+ missing.push('preview.ts');
180
+ if (missing.length > 0) {
181
+ return [{ name: '.storybook/', status: 'fail', message: `Missing: ${missing.join(', ')}`, fixable: true }];
182
+ }
183
+ return [{ name: '.storybook/', status: 'pass', message: 'main.ts and preview.ts present', fixable: false }];
184
+ },
185
+ fix(wsPath) {
186
+ const templateDir = join(templatesPath(), 'project', '.storybook');
187
+ const storybookDir = join(wsPath, '.storybook');
188
+ cpSync(templateDir, storybookDir, { recursive: true });
189
+ },
190
+ };
191
+ // ── Check 6: vitest.config.ts ──────────────────────────────────────────
192
+ const vitestConfigCheck = {
193
+ name: 'vitest.config.ts',
194
+ run(wsPath) {
195
+ if (!existsSync(join(wsPath, 'vitest.config.ts'))) {
196
+ return [{ name: 'vitest.config.ts', status: 'fail', message: 'File is missing', fixable: true }];
197
+ }
198
+ return [{ name: 'vitest.config.ts', status: 'pass', message: 'File exists', fixable: false }];
199
+ },
200
+ fix(wsPath) {
201
+ const templateFile = join(templatesPath(), 'project', 'vitest.config.ts');
202
+ cpSync(templateFile, join(wsPath, 'vitest.config.ts'));
203
+ },
204
+ };
205
+ // ── Check 7: Package alpha block ───────────────────────────────────────
168
206
  function getPackagesSafe(wsPath) {
169
207
  try {
170
208
  const packageJsonContent = readFileSync(join(wsPath, 'package.json'), 'utf-8');
@@ -349,6 +387,8 @@ export const checks = [
349
387
  rootPackageJsonCheck,
350
388
  tsconfigCheck,
351
389
  pluginsServerCheck,
390
+ storybookCheck,
391
+ vitestConfigCheck,
352
392
  packageAlphaBlockCheck,
353
393
  componentEntryFileCheck,
354
394
  nodeVersionCheck,
@@ -45,8 +45,8 @@ const createStyles = (config) => {
45
45
  const template = handlebars.compile(readFileSync(join(componentTemplatesPath(), '.styles.ts.hbs'), 'utf-8'));
46
46
  return template(config);
47
47
  };
48
- const createTest = (config) => {
49
- const template = handlebars.compile(readFileSync(join(componentTemplatesPath(), '.test.ts.hbs'), 'utf-8'));
48
+ const createStory = (config) => {
49
+ const template = handlebars.compile(readFileSync(join(componentTemplatesPath(), '.stories.ts.hbs'), 'utf-8'));
50
50
  return template(config);
51
51
  };
52
52
  const appendToPublicAPIExports = (config) => {
@@ -69,6 +69,7 @@ const createComponentFiles = async (name, componentDirPath, wsPath) => {
69
69
  await Promise.all([
70
70
  writeFile(join(componentDirPath, `${componentSelector}.ts`), createTs(config), 'utf-8'),
71
71
  writeFile(join(componentDirPath, `${componentSelector}.styles.ts`), await prettify(createStyles(config), 'typescript'), 'utf-8'),
72
+ writeFile(join(componentDirPath, `${componentSelector}.stories.ts`), await prettify(createStory(config), 'typescript'), 'utf-8'),
72
73
  ]);
73
74
  const publicAPIPath = join(wsPath, './public-api.ts');
74
75
  const publicAPIContent = readFileSync(publicAPIPath, 'utf-8');
@@ -0,0 +1,23 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Resolves the workspace-local CLI binary path.
5
+ * Returns the resolved path if delegation should happen, or null if not.
6
+ *
7
+ * Delegation happens when:
8
+ * 1. A local neutrinos-cli binary exists in the workspace's node_modules
9
+ * 2. It resolves to a different path than the currently executing script
10
+ *
11
+ * Returns null when:
12
+ * - No local binary exists (e.g., running `neutrinos new` outside a workspace)
13
+ * - The local binary IS the current script (already running locally, prevents infinite recursion)
14
+ */
15
+ export const resolveLocalCli = (workingDir, currentScriptPath) => {
16
+ const localCliEntry = join(workingDir, 'node_modules', 'neutrinos-cli', 'dist', 'src', 'bin', 'cli.js');
17
+ if (!existsSync(localCliEntry))
18
+ return null;
19
+ const localScript = realpathSync(localCliEntry);
20
+ if (currentScriptPath === localScript)
21
+ return null;
22
+ return localScript;
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neutrinos-cli",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "neutrinos": "./dist/src/bin/cli.js"
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components';
2
+ import { expect, fn, userEvent } from 'storybook/test';
3
+ import { html } from 'lit';
4
+ import './{{componentSelector}}';
5
+
6
+ const TAG = '{{componentSelectorPrefix}}-{{componentSelector}}-1\\.0\\.0';
7
+
8
+ const meta: Meta = {
9
+ title: 'Components/{{componentLabel}}',
10
+ tags: ['autodocs'],
11
+ render: (args) =>
12
+ html`<{{componentSelectorPrefix}}-{{componentSelector}}-1.0.0
13
+ .value=${args.value}
14
+ ?disabled=${args.disabled}
15
+ ?readonly=${args.readonly}
16
+ @onIncrement=${args.onIncrement}
17
+ @onDecrement=${args.onDecrement}
18
+ ></{{componentSelectorPrefix}}-{{componentSelector}}-1.0.0>`,
19
+ argTypes: {
20
+ value: { control: 'number' },
21
+ disabled: { control: 'boolean' },
22
+ readonly: { control: 'boolean' },
23
+ },
24
+ args: {
25
+ value: 0,
26
+ disabled: false,
27
+ readonly: false,
28
+ onIncrement: fn(),
29
+ onDecrement: fn(),
30
+ },
31
+ };
32
+ export default meta;
33
+
34
+ type Story = StoryObj;
35
+
36
+ export const Default: Story = {
37
+ play: async ({ canvasElement, args }) => {
38
+ const el = canvasElement.querySelector(TAG)!;
39
+ const buttons = el.shadowRoot!.querySelectorAll('button');
40
+ const incrementBtn = buttons[0]!;
41
+ const decrementBtn = buttons[1]!;
42
+
43
+ await userEvent.click(incrementBtn);
44
+ await expect(args.onIncrement).toHaveBeenCalled();
45
+
46
+ await userEvent.click(decrementBtn);
47
+ await expect(args.onDecrement).toHaveBeenCalled();
48
+ },
49
+ };
50
+
51
+ export const Disabled: Story = {
52
+ args: { disabled: true },
53
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Storybook decorator that intercepts alpha* and http-plugin* CustomEvents
3
+ * and renders a scrollable event log panel below the story.
4
+ *
5
+ * Enable per-story via `parameters.eventLogger: true` or globally in preview.ts.
6
+ */
7
+ import { html, type TemplateResult } from 'lit';
8
+ import type { DecoratorFunction } from 'storybook/internal/types';
9
+
10
+ interface LogEntry {
11
+ time: string;
12
+ type: string;
13
+ detail: string;
14
+ }
15
+
16
+ const MAX_ENTRIES = 200;
17
+
18
+ function createEventLogPanel(): { container: HTMLDivElement; append: (entry: LogEntry) => void; clear: () => void } {
19
+ const container = document.createElement('div');
20
+ container.className = 'event-logger-panel';
21
+ Object.assign(container.style, {
22
+ marginTop: '16px',
23
+ border: '1px solid #333',
24
+ borderRadius: '4px',
25
+ background: '#1e1e1e',
26
+ color: '#d4d4d4',
27
+ fontFamily: "'IBM Plex Mono', 'SF Mono', 'Fira Code', monospace",
28
+ fontSize: '12px',
29
+ maxHeight: '240px',
30
+ overflowY: 'auto',
31
+ padding: '0',
32
+ });
33
+
34
+ // Header
35
+ const header = document.createElement('div');
36
+ Object.assign(header.style, {
37
+ display: 'flex',
38
+ justifyContent: 'space-between',
39
+ alignItems: 'center',
40
+ padding: '6px 10px',
41
+ background: '#2d2d2d',
42
+ borderBottom: '1px solid #333',
43
+ position: 'sticky',
44
+ top: '0',
45
+ });
46
+ header.innerHTML = '<span style="font-weight:600;color:#569cd6">Event Log</span>';
47
+
48
+ const clearBtn = document.createElement('button');
49
+ clearBtn.textContent = 'Clear';
50
+ Object.assign(clearBtn.style, {
51
+ background: 'transparent',
52
+ border: '1px solid #555',
53
+ color: '#aaa',
54
+ borderRadius: '3px',
55
+ cursor: 'pointer',
56
+ padding: '2px 8px',
57
+ fontSize: '11px',
58
+ });
59
+ header.appendChild(clearBtn);
60
+ container.appendChild(header);
61
+
62
+ const logBody = document.createElement('div');
63
+ logBody.style.padding = '4px 0';
64
+ container.appendChild(logBody);
65
+
66
+ let entryCount = 0;
67
+
68
+ function append(entry: LogEntry) {
69
+ if (entryCount >= MAX_ENTRIES) {
70
+ logBody.firstChild?.remove();
71
+ } else {
72
+ entryCount++;
73
+ }
74
+
75
+ const row = document.createElement('div');
76
+ Object.assign(row.style, {
77
+ padding: '2px 10px',
78
+ borderBottom: '1px solid #2a2a2a',
79
+ display: 'flex',
80
+ gap: '10px',
81
+ });
82
+ row.innerHTML = `<span style="color:#6a9955;white-space:nowrap">${entry.time}</span><span style="color:#dcdcaa;min-width:180px">${entry.type}</span><span style="color:#ce9178;word-break:break-all">${entry.detail}</span>`;
83
+ logBody.appendChild(row);
84
+ container.scrollTop = container.scrollHeight;
85
+ }
86
+
87
+ function clear() {
88
+ logBody.innerHTML = '';
89
+ entryCount = 0;
90
+ }
91
+
92
+ clearBtn.addEventListener('click', clear);
93
+
94
+ return { container, append, clear };
95
+ }
96
+
97
+ export const eventLoggerDecorator: DecoratorFunction = (storyFn, context) => {
98
+ const enabled = context.parameters?.eventLogger !== false;
99
+ if (!enabled) return storyFn();
100
+
101
+ // Return a lit template that wraps the story and appends the log panel
102
+ const storyResult = storyFn() as TemplateResult;
103
+
104
+ // We need to use a connected callback approach since we need real DOM
105
+ const wrapper = document.createElement('div');
106
+
107
+ // Render story content placeholder
108
+ const storySlot = document.createElement('div');
109
+ wrapper.appendChild(storySlot);
110
+
111
+ const { container: logPanel, append } = createEventLogPanel();
112
+ wrapper.appendChild(logPanel);
113
+
114
+ const EVENT_PATTERN = /^(alpha|http-plugin)/;
115
+ const controller = new AbortController();
116
+
117
+ const listener = (e: Event) => {
118
+ if (EVENT_PATTERN.test(e.type)) {
119
+ const now = new Date();
120
+ const time = `${now.toLocaleTimeString()}.${String(now.getMilliseconds()).padStart(3, '0')}`;
121
+ let detail = '';
122
+ if (e instanceof CustomEvent && e.detail != null) {
123
+ try {
124
+ detail = typeof e.detail === 'string' ? e.detail : JSON.stringify(e.detail, null, 0);
125
+ if (detail.length > 300) detail = detail.slice(0, 300) + '…';
126
+ } catch {
127
+ detail = String(e.detail);
128
+ }
129
+ }
130
+ append({ time, type: e.type, detail });
131
+ }
132
+ };
133
+
134
+ // Capture all events on window
135
+ const origDispatch = window.dispatchEvent.bind(window);
136
+ window.dispatchEvent = function (event: Event) {
137
+ listener(event);
138
+ return origDispatch(event);
139
+ };
140
+
141
+ // Cleanup when story unmounts
142
+ const observer = new MutationObserver(() => {
143
+ if (!document.contains(wrapper)) {
144
+ controller.abort();
145
+ window.dispatchEvent = origDispatch;
146
+ observer.disconnect();
147
+ }
148
+ });
149
+ // Start observing after a tick to let DOM settle
150
+ setTimeout(() => observer.observe(document.body, { childList: true, subtree: true }), 100);
151
+
152
+ return html`<div>${storyResult} ${logPanel}</div>`;
153
+ };
@@ -0,0 +1,19 @@
1
+ import type { StorybookConfig } from '@storybook/web-components-vite';
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../packages/**/*.stories.ts'],
5
+ framework: '@storybook/web-components-vite',
6
+ addons: ['@storybook/addon-vitest'],
7
+ async viteFinal(config) {
8
+ return {
9
+ ...config,
10
+ esbuild: {
11
+ ...config.esbuild,
12
+ target: 'es2020',
13
+ keepNames: true,
14
+ },
15
+ };
16
+ },
17
+ };
18
+
19
+ export default config;
@@ -0,0 +1,12 @@
1
+ import type { Preview } from '@storybook/web-components-vite';
2
+ import { eventLoggerDecorator } from './decorators/event-logger';
3
+
4
+ const preview: Preview = {
5
+ parameters: {
6
+ controls: { expanded: true },
7
+ eventLogger: true,
8
+ },
9
+ decorators: [eventLoggerDecorator],
10
+ };
11
+
12
+ export default preview;
@@ -23,5 +23,5 @@
23
23
  "noUnusedParameters": true,
24
24
  "noFallthroughCasesInSwitch": true
25
25
  },
26
- "include": ["packages", "index.ts"]
26
+ "include": ["packages"]
27
27
  }
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { playwright } from '@vitest/browser-playwright';
3
+ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
4
+
5
+ export default defineConfig({
6
+ test: {
7
+ projects: [
8
+ {
9
+ plugins: [
10
+ storybookTest({ configDir: import.meta.dirname + '/.storybook' }),
11
+ ],
12
+ test: {
13
+ name: 'storybook',
14
+ browser: {
15
+ enabled: true,
16
+ provider: playwright({}),
17
+ headless: true,
18
+ instances: [{ browser: 'chromium' }],
19
+ },
20
+ },
21
+ },
22
+ ],
23
+ },
24
+ });
@@ -1,15 +0,0 @@
1
- import { expect, fixture } from '@open-wc/testing';
2
- import { html } from 'lit';
3
- import './{{componentSelector}}';
4
- import { {{componentClassName}} } from './{{componentSelector}}';
5
-
6
- describe('alpha-hue', () => {
7
- beforeEach(async () => {
8
- // Things to do before each test
9
- });
10
-
11
- it('should render', async () => {
12
- const element: Hue = await fixture(html\`<{{componentSelector}}></{{componentSelector}}>\`);
13
- expect(element).shadowDom.to.equalSnapshot();
14
- });
15
- });
@@ -1,24 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Components</title>
7
- <style alphaGlobalStyle>
8
- /*
9
- Global styles with alphaGlobalStyle directive. This will be available in all components.
10
- example render output: html`<div class="small-1">Hello World</div>`
11
- */
12
- .small-1 {
13
- font-size: 12px;
14
- }
15
- .small-2 {
16
- color: red;
17
- }
18
- </style>
19
- </head>
20
- <body style="padding: 1rem; box-sizing: border-box">
21
- <div id="app"></div>
22
- <script type="module" src="./index.ts"></script>
23
- </body>
24
- </html>
@@ -1,86 +0,0 @@
1
- import * as _cs from './public-api.ts';
2
- declare global {
3
- interface Window {
4
- alpha: any;
5
- ap: any;
6
- }
7
- }
8
-
9
- const setup = () => {
10
- const style = document.createElement('style');
11
- document.querySelector('head')?.appendChild(style);
12
- };
13
- /**
14
- * Store all the component instances in globalThis.aci
15
- * for testing purposes
16
- */
17
- globalThis.aci = {} as any;
18
- const addComponent = (name: string, { inputs, outputs }: ComponentConfig) => {
19
- const customElem = document.createElement(name) as any;
20
- const fieldSet = document.createElement('fieldset');
21
- const legend = document.createElement('legend');
22
- legend.textContent = name;
23
- fieldSet.appendChild(legend);
24
- const key = name.slice('alpha'.length + 1);
25
- (globalThis.aci[key] = globalThis.aci[key] || []).push(customElem);
26
- for (let [key, val] of Object.entries(inputs || {})) {
27
- customElem[key] = val;
28
- const input = document.createElement('input');
29
- input.type = 'text';
30
- input.value = val;
31
- input.placeholder = key;
32
- input.addEventListener('change', (e) => {
33
- customElem[key] = (e.target as any).value;
34
- });
35
- fieldSet.appendChild(input);
36
- }
37
- fieldSet.appendChild(customElem);
38
- const outputEvents = outputs?.events || ['change'];
39
- outputEvents.forEach((event) => {
40
- customElem.addEventListener(event, (e: any) => {
41
- console.log(`[${name}][event:${event}]`, e);
42
- });
43
- });
44
- document.body.appendChild(fieldSet);
45
- };
46
-
47
- const components: Record<string, ComponentConfig> = (() => {
48
- console.log(_cs);
49
- const comps = {};
50
- for (const [_, _class] of Object.entries(_cs)) {
51
- const inputs = Reflect.getMetadata('AlphaAttribute', _class.prototype);
52
- const c = Reflect.getMetadata('AlphaComponent', _class.prototype);
53
- const selector = `${c.selector}-${c.componentVersion}`;
54
- comps[selector] = {
55
- inputs: inputs?.reduce((acc, { fieldMappings, defaultValue }) => {
56
- acc[fieldMappings] = defaultValue ?? '';
57
- return acc;
58
- }, {}),
59
- outputs: {
60
- events: ['change'],
61
- },
62
- };
63
- }
64
- return comps;
65
- })();
66
- document.addEventListener('DOMContentLoaded', async () => {
67
- setup();
68
- for (let [name, config] of Object.entries(components)) {
69
- addComponent(name, config);
70
- }
71
- });
72
-
73
- interface ComponentConfig {
74
- class?: string;
75
- inputs?: {
76
- disabled?: boolean;
77
- readonly?: boolean;
78
- value?: any;
79
- name?: any;
80
- id?: any;
81
- [key: string]: any;
82
- };
83
- outputs?: {
84
- events: string[];
85
- };
86
- }