@sociallane/elements 1.0.12 → 1.0.14

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.
@@ -9,6 +9,7 @@ import { readdirSync, existsSync } from 'fs';
9
9
  import path from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { spawnSync } from 'child_process';
12
+ import { readWidgetManifest } from './lib/widgets-manifest.js';
12
13
 
13
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
15
  const pluginRoot = path.resolve(__dirname, '..');
@@ -19,10 +20,27 @@ if (!existsSync(npmWidgetsDir)) {
19
20
  process.exit(1);
20
21
  }
21
22
 
22
- const dirs = readdirSync(npmWidgetsDir, { withFileTypes: true })
23
+ const manifest = readWidgetManifest(pluginRoot);
24
+ if (!manifest || !Array.isArray(manifest.widgets) || manifest.widgets.length === 0) {
25
+ console.error('Widget manifest not found or empty. Run npm run sync-widgets first.');
26
+ process.exit(1);
27
+ }
28
+
29
+ const manifestSlugs = manifest.widgets.map((widget) => widget.slug).sort((a, b) => a.localeCompare(b));
30
+ const availableDirs = readdirSync(npmWidgetsDir, { withFileTypes: true })
23
31
  .filter((d) => d.isDirectory())
24
- .map((d) => d.name)
25
- .sort((a, b) => a.localeCompare(b));
32
+ .map((d) => d.name);
33
+ const dirs = manifestSlugs.filter((slug) => availableDirs.includes(slug));
34
+
35
+ const missingBuiltPackages = manifestSlugs.filter((slug) => !availableDirs.includes(slug));
36
+ if (missingBuiltPackages.length > 0) {
37
+ console.error(
38
+ 'Missing built widget packages in packages/npm-widgets/:',
39
+ missingBuiltPackages.slice(0, 20).join(', ') + (missingBuiltPackages.length > 20 ? '...' : '')
40
+ );
41
+ console.error('Run npm run build:widget-packages first.');
42
+ process.exit(1);
43
+ }
26
44
 
27
45
  if (dirs.length === 0) {
28
46
  console.error('No widget packages in packages/npm-widgets/');
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Release gate checks:
4
+ * - canonical widget manifest exists and matches widget folders
5
+ * - CLI sample slugs in install.js exist in manifest
6
+ * - npm registry versions match root package version (strict mode)
7
+ *
8
+ * Usage:
9
+ * node scripts/release-gate.js --strict
10
+ * node scripts/release-gate.js --strict --skip-registry
11
+ */
12
+
13
+ import { existsSync, readFileSync } from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import { spawnSync } from 'child_process';
17
+ import { getWidgetSlugsFromSource, readWidgetManifest, syncWidgetManifest } from './lib/widgets-manifest.js';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const pluginRoot = path.resolve(__dirname, '..');
21
+ const args = process.argv.slice(2);
22
+ const strict = args.includes('--strict');
23
+ const skipRegistry = args.includes('--skip-registry');
24
+
25
+ function runNpmViewVersion(pkgName) {
26
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
27
+ const result = spawnSync(
28
+ npmCmd,
29
+ ['view', pkgName, 'version', '--registry=https://registry.npmjs.org', '--loglevel=error'],
30
+ {
31
+ cwd: pluginRoot,
32
+ encoding: 'utf8',
33
+ shell: true,
34
+ }
35
+ );
36
+
37
+ if (result.status !== 0) {
38
+ const stderr = (result.stderr || '')
39
+ .split('\n')
40
+ .map((line) => line.trim())
41
+ .find((line) => line !== '');
42
+ return { ok: false, version: null, error: stderr || 'npm view failed' };
43
+ }
44
+
45
+ const version = (result.stdout || '').trim().split('\n').pop().trim();
46
+ if (!version) {
47
+ return { ok: false, version: null, error: 'empty version response' };
48
+ }
49
+
50
+ return { ok: true, version, error: null };
51
+ }
52
+
53
+ function getRootVersion() {
54
+ const packageJsonPath = path.join(pluginRoot, 'package.json');
55
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
56
+ return typeof pkg.version === 'string' ? pkg.version : '';
57
+ }
58
+
59
+ function checkCliExampleSlugs(manifestSlugs, issues) {
60
+ const installScriptPath = path.join(pluginRoot, 'scripts', 'install.js');
61
+ if (!existsSync(installScriptPath)) {
62
+ issues.push('scripts/install.js not found for CLI example validation.');
63
+ return;
64
+ }
65
+
66
+ const content = readFileSync(installScriptPath, 'utf8');
67
+ const expectedSampleSlugs = ['faq-stacked', 'hero-split', 'content-block'];
68
+
69
+ for (const slug of expectedSampleSlugs) {
70
+ if (!manifestSlugs.includes(slug)) {
71
+ issues.push(`CLI sample slug "${slug}" is not present in canonical widget manifest.`);
72
+ }
73
+ }
74
+
75
+ if (!content.includes('npx @sociallane/elements add')) {
76
+ issues.push('CLI help examples do not include add command examples.');
77
+ }
78
+ }
79
+
80
+ function checkDocumentationExamples(manifestSlugs, manifestPackages, issues) {
81
+ const docsFiles = [
82
+ path.join(pluginRoot, 'docs', 'install-new-instance.md'),
83
+ path.join(pluginRoot, 'docs', 'package-installation.md'),
84
+ path.join(pluginRoot, 'docs', 'npm-widget-cheatsheet.md'),
85
+ ];
86
+
87
+ for (const docsFile of docsFiles) {
88
+ if (!existsSync(docsFile)) {
89
+ continue;
90
+ }
91
+
92
+ const content = readFileSync(docsFile, 'utf8');
93
+
94
+ // Validate widget package references in docs.
95
+ const packageMatches = content.match(/@sociallane\/widget-[a-z0-9-]+/g) || [];
96
+ for (const pkg of packageMatches) {
97
+ if (!manifestPackages.has(pkg)) {
98
+ issues.push(`Docs reference unknown widget package "${pkg}" (${path.basename(docsFile)}).`);
99
+ }
100
+ }
101
+
102
+ // Validate npx add slug examples in docs.
103
+ const addCommandMatches =
104
+ content.match(/npx\s+@sociallane\/elements\s+add(?:\s+--[a-z-]+(?:\s+[^\s`]+)?)*\s+[a-z0-9-][^\n`]*/g) || [];
105
+ for (const cmd of addCommandMatches) {
106
+ const parts = cmd.split(/\s+/).slice(3); // strip: npx @sociallane/elements add
107
+ const slugs = parts.filter((part, idx) => {
108
+ if (!part || part.startsWith('--')) return false;
109
+ // Skip values for --target
110
+ const prev = parts[idx - 1];
111
+ if (prev === '--target') return false;
112
+ return /^[a-z0-9-]+$/.test(part);
113
+ });
114
+
115
+ for (const slug of slugs) {
116
+ if (/^slug\d*$/i.test(slug)) {
117
+ continue; // placeholder tokens in docs examples
118
+ }
119
+ if (!manifestSlugs.has(slug)) {
120
+ issues.push(`Docs add command references unknown slug "${slug}" (${path.basename(docsFile)}).`);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ function main() {
128
+ const issues = [];
129
+
130
+ // Keep manifest current as part of the gate.
131
+ syncWidgetManifest(pluginRoot);
132
+ const manifest = readWidgetManifest(pluginRoot);
133
+ if (!manifest || !Array.isArray(manifest.widgets) || manifest.widgets.length === 0) {
134
+ issues.push('Widget manifest is missing or empty. Run npm run manifest:widgets.');
135
+ }
136
+
137
+ const manifestSlugs = manifest ? manifest.widgets.map((widget) => widget.slug) : [];
138
+ const manifestSlugSet = new Set(manifestSlugs);
139
+ const manifestPackageSet = new Set((manifest?.widgets ?? []).map((widget) => widget.package));
140
+ const sourceSlugs = getWidgetSlugsFromSource(pluginRoot);
141
+
142
+ if (manifestSlugs.length !== sourceSlugs.length) {
143
+ issues.push(
144
+ `Manifest/source widget count mismatch (manifest=${manifestSlugs.length}, source=${sourceSlugs.length}).`
145
+ );
146
+ }
147
+
148
+ const missingInManifest = sourceSlugs.filter((slug) => !manifestSlugs.includes(slug));
149
+ if (missingInManifest.length > 0) {
150
+ issues.push(`Widget slugs missing in manifest: ${missingInManifest.slice(0, 20).join(', ')}`);
151
+ }
152
+
153
+ const missingInSource = manifestSlugs.filter((slug) => !sourceSlugs.includes(slug));
154
+ if (missingInSource.length > 0) {
155
+ issues.push(`Manifest contains missing widget folders: ${missingInSource.slice(0, 20).join(', ')}`);
156
+ }
157
+
158
+ checkCliExampleSlugs(manifestSlugs, issues);
159
+ checkDocumentationExamples(manifestSlugSet, manifestPackageSet, issues);
160
+
161
+ const rootVersion = getRootVersion();
162
+ if (!rootVersion) {
163
+ issues.push('Could not determine root package version from package.json.');
164
+ }
165
+
166
+ if (strict && !skipRegistry) {
167
+ console.log(`Checking npm registry versions against @sociallane/elements@${rootVersion}...`);
168
+
169
+ const rootPkg = '@sociallane/elements';
170
+ const rootVersionCheck = runNpmViewVersion(rootPkg);
171
+ if (!rootVersionCheck.ok) {
172
+ issues.push(`${rootPkg}: ${rootVersionCheck.error}`);
173
+ } else if (rootVersionCheck.version !== rootVersion) {
174
+ issues.push(`${rootPkg} version mismatch (registry=${rootVersionCheck.version}, local=${rootVersion}).`);
175
+ }
176
+
177
+ for (const widget of manifest?.widgets ?? []) {
178
+ const view = runNpmViewVersion(widget.package);
179
+ if (!view.ok) {
180
+ issues.push(`${widget.package}: ${view.error}`);
181
+ continue;
182
+ }
183
+ if (view.version !== rootVersion) {
184
+ issues.push(`${widget.package} version mismatch (registry=${view.version}, expected=${rootVersion}).`);
185
+ }
186
+ }
187
+ }
188
+
189
+ if (issues.length > 0) {
190
+ console.error('Release gate failed:');
191
+ for (const issue of issues) {
192
+ console.error(`- ${issue}`);
193
+ }
194
+ process.exit(1);
195
+ }
196
+
197
+ console.log(`Release gate passed (${manifestSlugs.length} widgets validated).`);
198
+ }
199
+
200
+ main();
@@ -10,28 +10,15 @@
10
10
  import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { fileURLToPath } from 'url';
13
+ import { getWidgetSlugsFromSource, syncWidgetManifest } from './lib/widgets-manifest.js';
13
14
 
14
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
16
 
16
17
  const PLUGIN_ROOT = path.resolve(__dirname, '..');
17
- const WIDGETS_DIR = path.join(PLUGIN_ROOT, 'packages', 'widgets');
18
18
  const WIDGETS_JSON = path.join(PLUGIN_ROOT, 'widgets.json');
19
19
 
20
20
  function getSlugsOnDisk() {
21
- if (!fs.existsSync(WIDGETS_DIR)) {
22
- return [];
23
- }
24
- const entries = fs.readdirSync(WIDGETS_DIR, { withFileTypes: true });
25
- const slugs = [];
26
- for (const ent of entries) {
27
- if (!ent.isDirectory()) continue;
28
- const slug = ent.name;
29
- const phpFile = path.join(WIDGETS_DIR, slug, `${slug}.php`);
30
- if (fs.existsSync(phpFile)) {
31
- slugs.push(slug);
32
- }
33
- }
34
- return slugs.sort((a, b) => a.localeCompare(b));
21
+ return getWidgetSlugsFromSource(PLUGIN_ROOT);
35
22
  }
36
23
 
37
24
  function getExistingSlugs() {
@@ -64,6 +51,11 @@ function main() {
64
51
  if (added > 0 || removed > 0) {
65
52
  console.log(`widgets.json updated: ${added} added, ${removed} removed. Total: ${widgets.length}`);
66
53
  }
54
+
55
+ const manifest = syncWidgetManifest(PLUGIN_ROOT);
56
+ if (added > 0 || removed > 0) {
57
+ console.log(`widget manifest synced: ${manifest.widgets.length} widgets.`);
58
+ }
67
59
  }
68
60
 
69
61
  main();
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI smoke tests for scripts/install.js.
4
+ * Uses a fake npm binary so tests run quickly and offline.
5
+ */
6
+
7
+ import assert from 'assert/strict';
8
+ import { chmodSync, existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { spawnSync } from 'child_process';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const pluginRoot = path.resolve(__dirname, '..');
16
+ const cliPath = path.join(pluginRoot, 'scripts', 'install.js');
17
+
18
+ function fail(message, extra = '') {
19
+ const details = extra ? `\n${extra}` : '';
20
+ throw new Error(`${message}${details}`);
21
+ }
22
+
23
+ function runCli(args, options = {}) {
24
+ const result = spawnSync(process.execPath, [cliPath, ...args], {
25
+ cwd: options.cwd || pluginRoot,
26
+ env: options.env || process.env,
27
+ encoding: 'utf8',
28
+ });
29
+ return result;
30
+ }
31
+
32
+ function createFakeNpmBin(rootDir) {
33
+ const binDir = path.join(rootDir, 'fake-bin');
34
+ mkdirSync(binDir, { recursive: true });
35
+
36
+ const npmPath = path.join(binDir, 'npm');
37
+ writeFileSync(
38
+ npmPath,
39
+ [
40
+ '#!/usr/bin/env sh',
41
+ 'echo "fake npm $@" >> "$FAKE_NPM_LOG"',
42
+ 'exit 0',
43
+ '',
44
+ ].join('\n'),
45
+ 'utf8'
46
+ );
47
+ chmodSync(npmPath, 0o755);
48
+
49
+ return binDir;
50
+ }
51
+
52
+ function main() {
53
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'sociallane-cli-smoke-'));
54
+
55
+ try {
56
+ const fakeLog = path.join(tempRoot, 'fake-npm.log');
57
+ const fakeBin = createFakeNpmBin(tempRoot);
58
+ const env = {
59
+ ...process.env,
60
+ PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
61
+ FAKE_NPM_LOG: fakeLog,
62
+ SOCIALLANE_SKIP_SYNC_WIDGETS: '1',
63
+ };
64
+
65
+ // 1) help exits successfully
66
+ const help = runCli(['--help'], { env });
67
+ assert.equal(help.status, 0, `--help failed: ${help.stderr}`);
68
+ assert.match(help.stdout, /SocialLane Elements CLI/, '--help output missing CLI header');
69
+
70
+ // 2) unknown flag fails
71
+ const bad = runCli(['--not-a-real-flag'], { env });
72
+ assert.notEqual(bad.status, 0, 'Unknown flag should fail');
73
+
74
+ // 3) base install creates plugin with empty widgets.json and no imported widgets
75
+ const wpRoot = path.join(tempRoot, 'site');
76
+ const pluginDir = path.join(wpRoot, 'wp-content', 'plugins', 'sociallane-elements');
77
+ const base = runCli(['--base', pluginDir], { cwd: tempRoot, env });
78
+ assert.equal(base.status, 0, `--base failed:\n${base.stdout}\n${base.stderr}`);
79
+
80
+ const widgetsJsonPath = path.join(pluginDir, 'widgets.json');
81
+ assert.equal(existsSync(path.join(pluginDir, 'sociallane-elements.php')), true, 'Plugin bootstrap missing');
82
+ assert.equal(existsSync(widgetsJsonPath), true, 'widgets.json missing after base install');
83
+
84
+ const widgetsJson = JSON.parse(readFileSync(widgetsJsonPath, 'utf8'));
85
+ assert.deepEqual(widgetsJson.widgets, [], 'Base install should keep widgets.json empty');
86
+
87
+ const widgetsPackagesDir = path.join(pluginDir, 'packages', 'widgets');
88
+ assert.equal(existsSync(widgetsPackagesDir), true, 'packages/widgets should exist after base install');
89
+ const widgetsAfterBase = readdirSync(widgetsPackagesDir, { withFileTypes: true }).filter((entry) =>
90
+ entry.isDirectory()
91
+ );
92
+ assert.equal(widgetsAfterBase.length, 0, 'Base install should not import widget component folders');
93
+
94
+ // 4) add with explicit --target from one directory above plugins
95
+ const fromWpContent = path.join(wpRoot, 'wp-content');
96
+ const addTarget = runCli(['add', '--target', pluginDir, 'faq-stacked', 'hero-split'], {
97
+ cwd: fromWpContent,
98
+ env,
99
+ });
100
+ assert.equal(addTarget.status, 0, `add --target failed:\n${addTarget.stdout}\n${addTarget.stderr}`);
101
+
102
+ const widgetsJsonAfterAdd = JSON.parse(readFileSync(widgetsJsonPath, 'utf8'));
103
+ assert.deepEqual(
104
+ widgetsJsonAfterAdd.widgets,
105
+ ['faq-stacked', 'hero-split'],
106
+ 'add --target should set selected slugs'
107
+ );
108
+
109
+ assert.equal(
110
+ existsSync(path.join(pluginDir, 'packages', 'widgets', 'faq-stacked', 'faq-stacked.php')),
111
+ true,
112
+ 'faq-stacked folder missing after add'
113
+ );
114
+ assert.equal(
115
+ existsSync(path.join(pluginDir, 'packages', 'widgets', 'hero-split', 'hero-split.php')),
116
+ true,
117
+ 'hero-split folder missing after add'
118
+ );
119
+
120
+ // 5) add without --target from wp-content should auto-resolve plugin dir
121
+ const addAuto = runCli(['add', 'content-block'], { cwd: fromWpContent, env });
122
+ assert.equal(addAuto.status, 0, `add (auto target) failed:\n${addAuto.stdout}\n${addAuto.stderr}`);
123
+
124
+ const widgetsJsonAfterAuto = JSON.parse(readFileSync(widgetsJsonPath, 'utf8'));
125
+ assert.deepEqual(
126
+ widgetsJsonAfterAuto.widgets,
127
+ ['faq-stacked', 'hero-split', 'content-block'],
128
+ 'auto target add should append slug'
129
+ );
130
+
131
+ if (!existsSync(fakeLog)) {
132
+ fail('Fake npm was never invoked');
133
+ }
134
+
135
+ console.log('CLI smoke tests passed.');
136
+ } finally {
137
+ rmSync(tempRoot, { recursive: true, force: true });
138
+ }
139
+ }
140
+
141
+ main();
@@ -1,18 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Per-widget installer: run from a @sociallane/widget-<slug> package (npx).
4
- * Ensures SocialLane Elements plugin is installed, then copies this package's widget/
5
- * into the plugin's packages/widgets/<slug>, runs sync-widgets and npm install.
4
+ * Ensures SocialLane Elements plugin is installed in base mode, then delegates
5
+ * widget import to the main CLI add command.
6
6
  */
7
7
 
8
- import { cpSync, existsSync, mkdirSync, readFileSync } from 'fs';
8
+ import { existsSync, readFileSync } from 'fs';
9
9
  import { spawnSync } from 'child_process';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const packageRoot = path.resolve(__dirname);
15
- const widgetDir = path.join(packageRoot, 'widget');
16
15
 
17
16
  function getSlugFromPackage() {
18
17
  const pkgPath = path.join(packageRoot, 'package.json');
@@ -56,62 +55,29 @@ function resolvePluginDir() {
56
55
  return path.join(cwd, 'wp-content', 'plugins', 'sociallane-elements');
57
56
  }
58
57
 
59
- function main() {
60
- const slug = getSlugFromPackage();
61
- if (!existsSync(widgetDir)) {
62
- console.error('Widget folder not found:', widgetDir);
63
- process.exit(1);
64
- }
65
- if (!existsSync(path.join(widgetDir, `${slug}.php`))) {
66
- console.error('Invalid widget: missing', path.join(widgetDir, `${slug}.php`));
67
- process.exit(1);
58
+ function runNpx(args, cwd) {
59
+ const r = spawnSync('npx', ['--yes', ...args], {
60
+ cwd,
61
+ stdio: 'inherit',
62
+ shell: true,
63
+ });
64
+ if (r.status !== 0) {
65
+ process.exit(r.status ?? 1);
68
66
  }
67
+ }
69
68
 
69
+ function main() {
70
+ const slug = getSlugFromPackage();
70
71
  const pluginDir = resolvePluginDir();
71
- const widgetsDest = path.join(pluginDir, 'packages', 'widgets');
72
72
  const pluginExists = existsSync(path.join(pluginDir, 'sociallane-elements.php'));
73
73
 
74
74
  if (!pluginExists) {
75
75
  console.log('SocialLane Elements not found. Installing base plugin...');
76
- const r = spawnSync('npx', ['--yes', '@sociallane/elements', '--minimal'], {
77
- cwd: path.dirname(pluginDir),
78
- stdio: 'inherit',
79
- shell: true,
80
- });
81
- if (r.status !== 0) {
82
- process.exit(r.status ?? 1);
83
- }
76
+ runNpx(['@sociallane/elements', '--base', pluginDir], path.dirname(pluginDir));
84
77
  }
85
78
 
86
- if (!existsSync(widgetsDest)) {
87
- mkdirSync(widgetsDest, { recursive: true });
88
- }
89
-
90
- const dest = path.join(widgetsDest, slug);
91
- cpSync(widgetDir, dest, { recursive: true, force: true });
92
- console.log('Added widget:', slug);
93
-
94
- console.log('Updating widgets.json and building...');
95
- const syncResult = spawnSync('node', [path.join(pluginDir, 'scripts', 'sync-widgets.js')], {
96
- cwd: pluginDir,
97
- stdio: 'inherit',
98
- });
99
- if (syncResult.status !== 0) {
100
- process.exit(syncResult.status ?? 1);
101
- }
102
-
103
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
104
- const installResult = spawnSync(npmCmd, ['install'], {
105
- cwd: pluginDir,
106
- stdio: 'inherit',
107
- shell: true,
108
- });
109
- if (installResult.status !== 0) {
110
- if (installResult.error) {
111
- console.error('Failed to run npm:', installResult.error.message);
112
- }
113
- process.exit(installResult.status ?? 1);
114
- }
79
+ console.log('Adding widget:', slug);
80
+ runNpx(['@sociallane/elements', 'add', '--target', pluginDir, slug], path.dirname(pluginDir));
115
81
 
116
82
  console.log('');
117
83
  console.log('Done. Widget', slug, 'installed. Activate the plugin in WordPress → Plugins if needed.');