sb-mig 6.1.1 → 6.2.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/dist/api/inspect/component-usage-query.d.ts +4 -0
- package/dist/api/inspect/component-usage-query.js +72 -0
- package/dist/api/inspect/component-usage.d.ts +8 -0
- package/dist/api/inspect/component-usage.js +159 -0
- package/dist/api/inspect/component-usage.types.d.ts +61 -0
- package/dist/api/inspect/component-usage.types.js +1 -0
- package/dist/api/inspect/index.d.ts +3 -0
- package/dist/api/inspect/index.js +2 -0
- package/dist/api/managementApi.d.ts +12 -0
- package/dist/api/managementApi.js +2 -0
- package/dist/cli/cli-descriptions.d.ts +2 -1
- package/dist/cli/cli-descriptions.js +35 -0
- package/dist/cli/commands/inspect.d.ts +2 -0
- package/dist/cli/commands/inspect.js +91 -0
- package/dist/cli/index.js +33 -1
- package/package.json +1 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ComponentUsageQuery } from "./component-usage.types.js";
|
|
2
|
+
export declare const discoverComponentUsageQueryFiles: (queryName: string) => string[];
|
|
3
|
+
export declare const loadComponentUsageQueryFromPath: (queryPath: string) => Promise<ComponentUsageQuery>;
|
|
4
|
+
export declare const loadComponentUsageQuery: (queryNameOrPath: string) => Promise<ComponentUsageQuery>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import storyblokConfig from "../../config/config.js";
|
|
3
|
+
import { toImportSpecifier } from "../../utils/files.js";
|
|
4
|
+
import { safeGlobSync as globSync } from "../../utils/glob-utils.js";
|
|
5
|
+
import { normalizeDiscover } from "../../utils/path-utils.js";
|
|
6
|
+
const QUERY_EXTENSIONS = [
|
|
7
|
+
"sb.query.js",
|
|
8
|
+
"sb.query.cjs",
|
|
9
|
+
"sb.query.mjs",
|
|
10
|
+
];
|
|
11
|
+
const isDirectQueryPath = (queryNameOrPath) => queryNameOrPath.includes("/") ||
|
|
12
|
+
queryNameOrPath.includes("\\") ||
|
|
13
|
+
QUERY_EXTENSIONS.some((ext) => queryNameOrPath.endsWith(`.${ext}`));
|
|
14
|
+
const withoutKnownQueryExtension = (queryName) => {
|
|
15
|
+
for (const ext of QUERY_EXTENSIONS) {
|
|
16
|
+
const suffix = `.${ext}`;
|
|
17
|
+
if (queryName.endsWith(suffix)) {
|
|
18
|
+
return queryName.slice(0, -suffix.length);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return queryName;
|
|
22
|
+
};
|
|
23
|
+
const querySearchDirectories = () => storyblokConfig.componentsDirectories.filter((directory) => !directory.includes("node_modules"));
|
|
24
|
+
export const discoverComponentUsageQueryFiles = (queryName) => {
|
|
25
|
+
const rootDirectory = path.resolve(process.cwd(), "./");
|
|
26
|
+
const searchDirectories = querySearchDirectories();
|
|
27
|
+
const normalizedQueryName = withoutKnownQueryExtension(queryName);
|
|
28
|
+
return QUERY_EXTENSIONS.flatMap((ext) => {
|
|
29
|
+
const pattern = path.join(rootDirectory, normalizeDiscover({ segments: searchDirectories }), "**", `${normalizedQueryName}.${ext}`);
|
|
30
|
+
return globSync(pattern.replace(/\\/g, "/"), {
|
|
31
|
+
follow: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
function assertValidQuery(query, sourcePath) {
|
|
36
|
+
if (!query || typeof query !== "object") {
|
|
37
|
+
throw new Error(`Query file '${sourcePath}' must export an object.`);
|
|
38
|
+
}
|
|
39
|
+
const candidate = query;
|
|
40
|
+
if (typeof candidate.name !== "string" || candidate.name.length === 0) {
|
|
41
|
+
throw new Error(`Query file '${sourcePath}' must export a non-empty 'name'.`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof candidate.match !== "function") {
|
|
44
|
+
throw new Error(`Query file '${sourcePath}' must export a 'match' function.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export const loadComponentUsageQueryFromPath = async (queryPath) => {
|
|
48
|
+
const resolvedPath = path.isAbsolute(queryPath)
|
|
49
|
+
? queryPath
|
|
50
|
+
: path.resolve(process.cwd(), queryPath);
|
|
51
|
+
const module = await import(
|
|
52
|
+
/* @vite-ignore */ toImportSpecifier(resolvedPath));
|
|
53
|
+
const query = module.default || module;
|
|
54
|
+
assertValidQuery(query, resolvedPath);
|
|
55
|
+
return query;
|
|
56
|
+
};
|
|
57
|
+
export const loadComponentUsageQuery = async (queryNameOrPath) => {
|
|
58
|
+
if (!queryNameOrPath || queryNameOrPath.trim().length === 0) {
|
|
59
|
+
throw new Error("--query is required for inspect component-usage.");
|
|
60
|
+
}
|
|
61
|
+
if (isDirectQueryPath(queryNameOrPath)) {
|
|
62
|
+
return loadComponentUsageQueryFromPath(queryNameOrPath);
|
|
63
|
+
}
|
|
64
|
+
const discoveredFiles = discoverComponentUsageQueryFiles(queryNameOrPath);
|
|
65
|
+
if (discoveredFiles.length === 0) {
|
|
66
|
+
throw new Error(`No component usage query found for '${queryNameOrPath}'. Expected a file named '${queryNameOrPath}.sb.query.js', '${queryNameOrPath}.sb.query.cjs', or '${queryNameOrPath}.sb.query.mjs' in configured component directories.`);
|
|
67
|
+
}
|
|
68
|
+
if (discoveredFiles.length > 1) {
|
|
69
|
+
throw new Error(`Multiple component usage queries found for '${queryNameOrPath}': ${discoveredFiles.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
return loadComponentUsageQueryFromPath(discoveredFiles[0]);
|
|
72
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentUsageMatch, ComponentUsageQuery, ComponentUsageReport, ComponentUsageStoryRef, InspectFetchedStoriesArgs, InspectStoryblokStoriesArgs } from "./component-usage.types.js";
|
|
2
|
+
import type { RequestBaseConfig } from "../utils/request.js";
|
|
3
|
+
export declare const inspectStoryContent: ({ story, query, }: {
|
|
4
|
+
story: ComponentUsageStoryRef;
|
|
5
|
+
query: ComponentUsageQuery;
|
|
6
|
+
}) => Promise<ComponentUsageMatch[]>;
|
|
7
|
+
export declare const inspectFetchedStories: ({ stories, query, spaceId, filters, }: InspectFetchedStoriesArgs) => Promise<ComponentUsageReport>;
|
|
8
|
+
export declare const inspectStoryblokStories: (args: InspectStoryblokStoriesArgs, config: RequestBaseConfig) => Promise<ComponentUsageReport>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getAllStories, getStoryBySlug } from "../stories/stories.js";
|
|
2
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
const isComponentNode = (value) => isRecord(value) && typeof value.component === "string";
|
|
4
|
+
const normalizeStory = (storyData) => {
|
|
5
|
+
const story = storyData?.story || storyData || {};
|
|
6
|
+
return story;
|
|
7
|
+
};
|
|
8
|
+
const storyHasContent = (story) => isRecord(story.content);
|
|
9
|
+
const normalizeMatchDetails = (matchResult) => {
|
|
10
|
+
if (matchResult === true) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (isRecord(matchResult)) {
|
|
14
|
+
return matchResult;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
const createMatch = ({ node, context, matchResult, }) => {
|
|
19
|
+
const details = normalizeMatchDetails(matchResult);
|
|
20
|
+
const match = {
|
|
21
|
+
storyId: context.story.id,
|
|
22
|
+
storyName: context.story.name,
|
|
23
|
+
storySlug: context.story.slug,
|
|
24
|
+
storyFullSlug: context.story.full_slug,
|
|
25
|
+
component: node.component,
|
|
26
|
+
uid: typeof node._uid === "string" ? node._uid : undefined,
|
|
27
|
+
path: context.path,
|
|
28
|
+
parentComponent: typeof context.parent?.component === "string"
|
|
29
|
+
? context.parent.component
|
|
30
|
+
: undefined,
|
|
31
|
+
parentUid: typeof context.parent?._uid === "string"
|
|
32
|
+
? context.parent._uid
|
|
33
|
+
: undefined,
|
|
34
|
+
};
|
|
35
|
+
if (details) {
|
|
36
|
+
match.details = details;
|
|
37
|
+
}
|
|
38
|
+
return match;
|
|
39
|
+
};
|
|
40
|
+
const joinPath = (path, segment) => path.length > 0 ? `${path}.${segment}` : segment;
|
|
41
|
+
const walkValue = async ({ value, path, story, parent, ancestors, query, matches, }) => {
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (let i = 0; i < value.length; i++) {
|
|
44
|
+
await walkValue({
|
|
45
|
+
value: value[i],
|
|
46
|
+
path: `${path}[${i}]`,
|
|
47
|
+
story,
|
|
48
|
+
parent,
|
|
49
|
+
ancestors,
|
|
50
|
+
query,
|
|
51
|
+
matches,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!isRecord(value)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const currentParent = isComponentNode(value) ? value : parent;
|
|
60
|
+
const currentAncestors = isComponentNode(value)
|
|
61
|
+
? [...ancestors, value]
|
|
62
|
+
: ancestors;
|
|
63
|
+
if (isComponentNode(value)) {
|
|
64
|
+
const context = {
|
|
65
|
+
story,
|
|
66
|
+
path,
|
|
67
|
+
parent,
|
|
68
|
+
ancestors,
|
|
69
|
+
};
|
|
70
|
+
const matchResult = await query.match(value, context);
|
|
71
|
+
if (matchResult) {
|
|
72
|
+
matches.push(createMatch({
|
|
73
|
+
node: value,
|
|
74
|
+
context,
|
|
75
|
+
matchResult,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const [key, child] of Object.entries(value)) {
|
|
80
|
+
if (key === "_uid" || key === "component") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
await walkValue({
|
|
84
|
+
value: child,
|
|
85
|
+
path: joinPath(path, key),
|
|
86
|
+
story,
|
|
87
|
+
parent: currentParent,
|
|
88
|
+
ancestors: currentAncestors,
|
|
89
|
+
query,
|
|
90
|
+
matches,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
export const inspectStoryContent = async ({ story, query, }) => {
|
|
95
|
+
const matches = [];
|
|
96
|
+
if (!storyHasContent(story)) {
|
|
97
|
+
return matches;
|
|
98
|
+
}
|
|
99
|
+
await walkValue({
|
|
100
|
+
value: story.content,
|
|
101
|
+
path: "content",
|
|
102
|
+
story,
|
|
103
|
+
query,
|
|
104
|
+
matches,
|
|
105
|
+
ancestors: [],
|
|
106
|
+
});
|
|
107
|
+
return matches;
|
|
108
|
+
};
|
|
109
|
+
export const inspectFetchedStories = async ({ stories, query, spaceId, filters = {}, }) => {
|
|
110
|
+
const selectedStories = stories
|
|
111
|
+
.map(normalizeStory)
|
|
112
|
+
.filter((story) => !story.is_folder);
|
|
113
|
+
const matchesByStory = await Promise.all(selectedStories.map(async (story) => inspectStoryContent({ story, query })));
|
|
114
|
+
const matches = matchesByStory.flat();
|
|
115
|
+
const matchedStoryIds = new Set(matches.map((match) => match.storyId));
|
|
116
|
+
return {
|
|
117
|
+
queryName: query.name,
|
|
118
|
+
spaceId,
|
|
119
|
+
generatedAt: new Date().toISOString(),
|
|
120
|
+
filters,
|
|
121
|
+
totals: {
|
|
122
|
+
storiesScanned: selectedStories.length,
|
|
123
|
+
storiesMatched: matchedStoryIds.size,
|
|
124
|
+
matches: matches.length,
|
|
125
|
+
},
|
|
126
|
+
matches,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
const resolveStoryFetchOptions = (filters) => {
|
|
130
|
+
if (filters.startsWith) {
|
|
131
|
+
return { starts_with: filters.startsWith };
|
|
132
|
+
}
|
|
133
|
+
return {};
|
|
134
|
+
};
|
|
135
|
+
export const inspectStoryblokStories = async (args, config) => {
|
|
136
|
+
const { query, spaceId, filters } = args;
|
|
137
|
+
if (filters.withSlug && filters.withSlug.length > 0) {
|
|
138
|
+
const stories = await Promise.all(filters.withSlug.map((slug) => getStoryBySlug(slug, {
|
|
139
|
+
...config,
|
|
140
|
+
spaceId,
|
|
141
|
+
})));
|
|
142
|
+
return inspectFetchedStories({
|
|
143
|
+
stories: stories.filter(Boolean),
|
|
144
|
+
query,
|
|
145
|
+
spaceId,
|
|
146
|
+
filters,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const stories = await getAllStories({ options: resolveStoryFetchOptions(filters) }, {
|
|
150
|
+
...config,
|
|
151
|
+
spaceId,
|
|
152
|
+
});
|
|
153
|
+
return inspectFetchedStories({
|
|
154
|
+
stories,
|
|
155
|
+
query,
|
|
156
|
+
spaceId,
|
|
157
|
+
filters,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface ComponentUsageStoryRef {
|
|
2
|
+
id: number | string;
|
|
3
|
+
name?: string;
|
|
4
|
+
slug?: string;
|
|
5
|
+
full_slug?: string;
|
|
6
|
+
is_folder?: boolean;
|
|
7
|
+
content?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface ComponentUsageQueryContext {
|
|
10
|
+
story: ComponentUsageStoryRef;
|
|
11
|
+
path: string;
|
|
12
|
+
parent?: Record<string, unknown>;
|
|
13
|
+
ancestors: Array<Record<string, unknown>>;
|
|
14
|
+
}
|
|
15
|
+
export type ComponentUsageQueryMatchResult = boolean | Record<string, unknown> | null | undefined;
|
|
16
|
+
export interface ComponentUsageQuery {
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
match: (node: Record<string, any>, context: ComponentUsageQueryContext) => ComponentUsageQueryMatchResult | Promise<ComponentUsageQueryMatchResult>;
|
|
20
|
+
}
|
|
21
|
+
export interface ComponentUsageMatch {
|
|
22
|
+
storyId: number | string;
|
|
23
|
+
storyName?: string;
|
|
24
|
+
storySlug?: string;
|
|
25
|
+
storyFullSlug?: string;
|
|
26
|
+
component: string;
|
|
27
|
+
uid?: string;
|
|
28
|
+
path: string;
|
|
29
|
+
parentComponent?: string;
|
|
30
|
+
parentUid?: string;
|
|
31
|
+
details?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export interface ComponentUsageFilters {
|
|
34
|
+
all?: boolean;
|
|
35
|
+
withSlug?: string[];
|
|
36
|
+
startsWith?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ComponentUsageReport {
|
|
39
|
+
queryName: string;
|
|
40
|
+
spaceId: string;
|
|
41
|
+
generatedAt: string;
|
|
42
|
+
filters: ComponentUsageFilters;
|
|
43
|
+
totals: {
|
|
44
|
+
storiesScanned: number;
|
|
45
|
+
storiesMatched: number;
|
|
46
|
+
matches: number;
|
|
47
|
+
};
|
|
48
|
+
matches: ComponentUsageMatch[];
|
|
49
|
+
}
|
|
50
|
+
export interface InspectFetchedStoriesArgs {
|
|
51
|
+
stories: any[];
|
|
52
|
+
query: ComponentUsageQuery;
|
|
53
|
+
spaceId: string;
|
|
54
|
+
filters?: ComponentUsageFilters;
|
|
55
|
+
}
|
|
56
|
+
export interface InspectStoryblokStoriesArgs {
|
|
57
|
+
query: ComponentUsageQuery;
|
|
58
|
+
spaceId: string;
|
|
59
|
+
filters: ComponentUsageFilters;
|
|
60
|
+
includeFolders?: boolean;
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { inspectFetchedStories, inspectStoryblokStories, inspectStoryContent, } from "./component-usage.js";
|
|
2
|
+
export { discoverComponentUsageQueryFiles, loadComponentUsageQuery, loadComponentUsageQueryFromPath, } from "./component-usage-query.js";
|
|
3
|
+
export type { ComponentUsageFilters, ComponentUsageMatch, ComponentUsageQuery, ComponentUsageQueryContext, ComponentUsageReport, ComponentUsageStoryRef, } from "./component-usage.types.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as inspect from "./inspect/index.js";
|
|
1
2
|
import * as stories from "./stories/index.js";
|
|
2
3
|
export declare const managementApi: {
|
|
3
4
|
assets: {
|
|
@@ -31,6 +32,17 @@ export declare const managementApi: {
|
|
|
31
32
|
updateDatasource: import("./datasources/datasources.types.js").UpdateDatasource;
|
|
32
33
|
syncDatasources: import("./datasources/datasources.types.js").SyncDatasources;
|
|
33
34
|
};
|
|
35
|
+
inspect: {
|
|
36
|
+
inspectFetchedStories: ({ stories, query, spaceId, filters, }: import("./inspect/component-usage.types.js").InspectFetchedStoriesArgs) => Promise<inspect.ComponentUsageReport>;
|
|
37
|
+
inspectStoryblokStories: (args: import("./inspect/component-usage.types.js").InspectStoryblokStoriesArgs, config: import("./utils/request.js").RequestBaseConfig) => Promise<inspect.ComponentUsageReport>;
|
|
38
|
+
inspectStoryContent: ({ story, query, }: {
|
|
39
|
+
story: inspect.ComponentUsageStoryRef;
|
|
40
|
+
query: inspect.ComponentUsageQuery;
|
|
41
|
+
}) => Promise<inspect.ComponentUsageMatch[]>;
|
|
42
|
+
discoverComponentUsageQueryFiles: (queryName: string) => string[];
|
|
43
|
+
loadComponentUsageQuery: (queryNameOrPath: string) => Promise<inspect.ComponentUsageQuery>;
|
|
44
|
+
loadComponentUsageQueryFromPath: (queryPath: string) => Promise<inspect.ComponentUsageQuery>;
|
|
45
|
+
};
|
|
34
46
|
plugins: {
|
|
35
47
|
getAllPlugins: import("./plugins/plugins.types.js").GetAllPlugins;
|
|
36
48
|
getPlugin: import("./plugins/plugins.types.js").GetPlugin;
|
|
@@ -2,6 +2,7 @@ import * as assets from "./assets/index.js";
|
|
|
2
2
|
import * as auth from "./auth/index.js";
|
|
3
3
|
import * as components from "./components/index.js";
|
|
4
4
|
import * as datasources from "./datasources/index.js";
|
|
5
|
+
import * as inspect from "./inspect/index.js";
|
|
5
6
|
import * as plugins from "./plugins/index.js";
|
|
6
7
|
import * as presets from "./presets/index.js";
|
|
7
8
|
import * as roles from "./roles/index.js";
|
|
@@ -12,6 +13,7 @@ export const managementApi = {
|
|
|
12
13
|
auth: { ...auth },
|
|
13
14
|
components: { ...components },
|
|
14
15
|
datasources: { ...datasources },
|
|
16
|
+
inspect: { ...inspect },
|
|
15
17
|
plugins: { ...plugins },
|
|
16
18
|
presets: { ...presets },
|
|
17
19
|
roles: { ...roles },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export declare const mainDescription = "\n USAGE\n $ sb-mig [command]\n\n COMMANDS\n sync Synchronize components, roles, datasources, plugins, stories, and assets.\n copy Copy Storyblok stories or folders between spaces.\n discover Discover local components and migration config files.\n backup Back up Storyblok resources to local JSON files.\n migrate Run story or preset data migrations.\n language-publish-state Build a read-only story language publish-state map.\n story-versions Inspect raw Management API story version history for one story.\n published-layer-export Export draft/current and published story layers as JSON.\n remove Remove components or stories from a Storyblok space.\n revert Restore stories from a local story backup file.\n migrations Recognize migration commands to run for a package upgrade.\n init Initialize project Storyblok environment settings.\n debug Output extra debugging information.\n help Show this screen.\n\n EXAMPLES\n $ sb-mig sync components --all\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --dry-run\n $ sb-mig copy stories --sourceSpace 12345 --targetSpace 67890 --what folder/* --where target-folder\n";
|
|
1
|
+
export declare const mainDescription = "\n USAGE\n $ sb-mig [command]\n\n COMMANDS\n sync Synchronize components, roles, datasources, plugins, stories, and assets.\n copy Copy Storyblok stories or folders between spaces.\n inspect Inspect Storyblok content without writing changes.\n discover Discover local components and migration config files.\n backup Back up Storyblok resources to local JSON files.\n migrate Run story or preset data migrations.\n language-publish-state Build a read-only story language publish-state map.\n story-versions Inspect raw Management API story version history for one story.\n published-layer-export Export draft/current and published story layers as JSON.\n remove Remove components or stories from a Storyblok space.\n revert Restore stories from a local story backup file.\n migrations Recognize migration commands to run for a package upgrade.\n init Initialize project Storyblok environment settings.\n debug Output extra debugging information.\n help Show this screen.\n\n EXAMPLES\n $ sb-mig sync components --all\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --dry-run\n $ sb-mig inspect component-usage --from 12345 --all --query flex-group-width-child\n $ sb-mig copy stories --sourceSpace 12345 --targetSpace 67890 --what folder/* --where target-folder\n";
|
|
2
|
+
export declare const inspectDescription = "\n USAGE\n $ sb-mig inspect component-usage --from [spaceId] --all --query [query-name]\n $ sb-mig inspect component-usage --from [spaceId] --withSlug [full_slug] --query [query-name]\n $ sb-mig inspect component-usage --from [spaceId] --startsWith [prefix] --query [query-name]\n\n DESCRIPTION\n Inspect Storyblok story content with a local component usage query file.\n This command is read-only against Storyblok and writes a local JSON file only when --outputPath is passed.\n\n COMMANDS\n component-usage Find component usage patterns in selected stories.\n\n FLAGS\n --from Source space ID to inspect. Falls back to configured spaceId.\n --all Inspect all non-folder stories.\n --withSlug Exact story full_slug to inspect. Can be repeated.\n --startsWith Filter stories by starts_with prefix.\n --query Query file name or path. Looks for *.sb.query.js, *.sb.query.cjs, or *.sb.query.mjs.\n --outputPath Optional file path for JSON report output.\n\n SIDE EFFECTS\n Read-only against Storyblok. Writes a local JSON report only when --outputPath is passed.\n\n GOTCHAS\n Pass exactly one selection mode: --all, --withSlug, or --startsWith.\n A query file must default-export an object with name and match(node, context).\n TypeScript query files are planned, but the first implementation supports JS/CJS/MJS query files.\n\n EXAMPLES\n $ sb-mig inspect component-usage --from 12345 --all --query flex-group-width-child\n $ sb-mig inspect component-usage --from 12345 --startsWith landing-pages --query ./queries/flex-group-width-child.sb.query.js --outputPath sbmig/usage/flex-group-width-child.json\n";
|
|
2
3
|
export declare const storyVersionsDescription = "\n USAGE\n $ sb-mig story-versions --from [spaceId] --storyId [storyId]\n $ sb-mig story-versions --from [spaceId] --withSlug [full_slug]\n\n DESCRIPTION\n Read Storyblok Management API story_versions for a single story.\n This command is read-only and is meant for inspecting version status values and content shape.\n\n FLAGS\n --from Source space ID to inspect. Required.\n --storyId Story ID to inspect. Required unless --withSlug is passed.\n --withSlug Story full_slug to resolve to a story ID. Required unless --storyId is passed.\n --showContent Include version content from Storyblok. Default: true.\n --page Versions page. Default: 1.\n --perPage Versions per page. Default: 25.\n --raw Print the raw Storyblok API response instead of the compact summary.\n --outputPath Optional file path for JSON output.\n\n SIDE EFFECTS\n Read-only against Storyblok. Writes a local JSON file only when --outputPath is passed.\n\n EXAMPLES\n $ sb-mig story-versions --from 12345 --storyId 98765\n $ sb-mig story-versions --from 12345 --withSlug tours/europe --raw --outputPath sbmig/story-versions/tours-europe.raw.json\n";
|
|
3
4
|
export declare const publishedLayerExportDescription = "\n USAGE\n $ sb-mig published-layer-export --from [spaceId] --all\n $ sb-mig published-layer-export --from [spaceId] --storyId [storyId]\n $ sb-mig published-layer-export --from [spaceId] --withSlug [full_slug]\n $ sb-mig published-layer-export --from [spaceId] --startsWith [prefix]\n\n DESCRIPTION\n Read selected Management API stories and their latest published Story Versions API content.\n This command is read-only against Storyblok. It writes JSON files for inspecting draft/current and published layers before changing migrate behavior.\n\n FLAGS\n --from Source space ID to inspect. Required.\n --all Export all non-folder stories.\n --storyId Story ID to export. Can be repeated.\n --withSlug Exact story full_slug to export. Can be repeated.\n --startsWith Filter stories by starts_with prefix.\n --fileName Stable output base name.\n --outputPath Output directory. Default: sbmig/published-layer-export.\n --versionsPerPage Story versions per page. Default: 25.\n --maxVersionPages Maximum Story Versions API pages to inspect per story. Default: 4.\n\n OUTPUT\n <name>---draft-current-full.json\n <name>---published-layer-full.json\n <name>---dual-layer-summary.json\n\n SIDE EFFECTS\n Read-only against Storyblok. Always writes local JSON export files.\n\n EXAMPLES\n $ sb-mig published-layer-export --from 12345 --withSlug translation-migration-testing/test-1/contact-us\n $ sb-mig published-layer-export --from 12345 --storyId 178888427520390\n $ sb-mig published-layer-export --from 12345 --startsWith translation-migration-testing --fileName translation-test\n";
|
|
4
5
|
export declare const languagePublishStateDescription = "\n USAGE\n $ sb-mig language-publish-state --from [spaceId]\n\n DESCRIPTION\n Read stories from a source Storyblok space and write a JSON map of default and translated language publication states.\n This command is read-only against Storyblok. It uses Management API for story listing and default-language state, and Delivery API for translated language published/draft comparisons.\n\n FLAGS\n --from Source space ID to inspect. Required.\n --accessToken Optional source space Delivery API access token override. Falls back to configured accessToken.\n --languages Languages to inspect: all, default,fr,de. Default: all.\n --withSlug Exact story full_slug to inspect. Can be repeated.\n --startsWith Filter stories by starts_with prefix.\n --fileName Stable output base name under sbmig/language-publish-state.\n --outputPath Explicit output path for the generated JSON file.\n\n SIDE EFFECTS\n Read-only against Storyblok. Writes a local JSON publish-state map.\n\n EXAMPLES\n $ sb-mig language-publish-state --from 12345 --startsWith about-ef --languages all --fileName about-ef-prod\n $ sb-mig language-publish-state --from 12345 --accessToken xxx --withSlug about-ef/testimonials --languages default,fr\n";
|
|
@@ -5,6 +5,7 @@ export const mainDescription = `
|
|
|
5
5
|
COMMANDS
|
|
6
6
|
sync Synchronize components, roles, datasources, plugins, stories, and assets.
|
|
7
7
|
copy Copy Storyblok stories or folders between spaces.
|
|
8
|
+
inspect Inspect Storyblok content without writing changes.
|
|
8
9
|
discover Discover local components and migration config files.
|
|
9
10
|
backup Back up Storyblok resources to local JSON files.
|
|
10
11
|
migrate Run story or preset data migrations.
|
|
@@ -21,8 +22,42 @@ export const mainDescription = `
|
|
|
21
22
|
EXAMPLES
|
|
22
23
|
$ sb-mig sync components --all
|
|
23
24
|
$ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --dry-run
|
|
25
|
+
$ sb-mig inspect component-usage --from 12345 --all --query flex-group-width-child
|
|
24
26
|
$ sb-mig copy stories --sourceSpace 12345 --targetSpace 67890 --what folder/* --where target-folder
|
|
25
27
|
`;
|
|
28
|
+
export const inspectDescription = `
|
|
29
|
+
USAGE
|
|
30
|
+
$ sb-mig inspect component-usage --from [spaceId] --all --query [query-name]
|
|
31
|
+
$ sb-mig inspect component-usage --from [spaceId] --withSlug [full_slug] --query [query-name]
|
|
32
|
+
$ sb-mig inspect component-usage --from [spaceId] --startsWith [prefix] --query [query-name]
|
|
33
|
+
|
|
34
|
+
DESCRIPTION
|
|
35
|
+
Inspect Storyblok story content with a local component usage query file.
|
|
36
|
+
This command is read-only against Storyblok and writes a local JSON file only when --outputPath is passed.
|
|
37
|
+
|
|
38
|
+
COMMANDS
|
|
39
|
+
component-usage Find component usage patterns in selected stories.
|
|
40
|
+
|
|
41
|
+
FLAGS
|
|
42
|
+
--from Source space ID to inspect. Falls back to configured spaceId.
|
|
43
|
+
--all Inspect all non-folder stories.
|
|
44
|
+
--withSlug Exact story full_slug to inspect. Can be repeated.
|
|
45
|
+
--startsWith Filter stories by starts_with prefix.
|
|
46
|
+
--query Query file name or path. Looks for *.sb.query.js, *.sb.query.cjs, or *.sb.query.mjs.
|
|
47
|
+
--outputPath Optional file path for JSON report output.
|
|
48
|
+
|
|
49
|
+
SIDE EFFECTS
|
|
50
|
+
Read-only against Storyblok. Writes a local JSON report only when --outputPath is passed.
|
|
51
|
+
|
|
52
|
+
GOTCHAS
|
|
53
|
+
Pass exactly one selection mode: --all, --withSlug, or --startsWith.
|
|
54
|
+
A query file must default-export an object with name and match(node, context).
|
|
55
|
+
TypeScript query files are planned, but the first implementation supports JS/CJS/MJS query files.
|
|
56
|
+
|
|
57
|
+
EXAMPLES
|
|
58
|
+
$ sb-mig inspect component-usage --from 12345 --all --query flex-group-width-child
|
|
59
|
+
$ sb-mig inspect component-usage --from 12345 --startsWith landing-pages --query ./queries/flex-group-width-child.sb.query.js --outputPath sbmig/usage/flex-group-width-child.json
|
|
60
|
+
`;
|
|
26
61
|
export const storyVersionsDescription = `
|
|
27
62
|
USAGE
|
|
28
63
|
$ sb-mig story-versions --from [spaceId] --storyId [storyId]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { loadComponentUsageQuery } from "../../api/inspect/component-usage-query.js";
|
|
2
|
+
import { inspectStoryblokStories } from "../../api/inspect/component-usage.js";
|
|
3
|
+
import { createAndSaveToFile } from "../../utils/files.js";
|
|
4
|
+
import Logger from "../../utils/logger.js";
|
|
5
|
+
import { apiConfig } from "../api-config.js";
|
|
6
|
+
import { getFrom } from "../utils/cli-utils.js";
|
|
7
|
+
const INSPECT_COMMANDS = {
|
|
8
|
+
componentUsage: "component-usage",
|
|
9
|
+
};
|
|
10
|
+
const normalizeWithSlug = (withSlugFlag) => {
|
|
11
|
+
if (Array.isArray(withSlugFlag)) {
|
|
12
|
+
return withSlugFlag.filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
if (typeof withSlugFlag === "string" && withSlugFlag.length > 0) {
|
|
15
|
+
return [withSlugFlag];
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
const countSelectionModes = ({ all, withSlug, startsWith, }) => [
|
|
20
|
+
Boolean(all),
|
|
21
|
+
Boolean(withSlug && withSlug.length > 0),
|
|
22
|
+
Boolean(startsWith),
|
|
23
|
+
].filter(Boolean).length;
|
|
24
|
+
const assertValidSelection = ({ all, withSlug, startsWith, }) => {
|
|
25
|
+
const selectedModes = countSelectionModes({ all, withSlug, startsWith });
|
|
26
|
+
if (selectedModes === 0) {
|
|
27
|
+
throw new Error("Missing story selection. Pass exactly one of --all, --withSlug, or --startsWith.");
|
|
28
|
+
}
|
|
29
|
+
if (selectedModes > 1) {
|
|
30
|
+
throw new Error("Pass only one story selection mode: --all, --withSlug, or --startsWith.");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const printComponentUsageSummary = (report) => {
|
|
34
|
+
Logger.log("Component usage inspection");
|
|
35
|
+
Logger.log(`Space: ${report.spaceId}`);
|
|
36
|
+
Logger.log(`Query: ${report.queryName}`);
|
|
37
|
+
Logger.log(`Stories scanned: ${report.totals.storiesScanned}`);
|
|
38
|
+
Logger.log(`Stories matched: ${report.totals.storiesMatched}`);
|
|
39
|
+
Logger.log(`Total matches: ${report.totals.matches}`);
|
|
40
|
+
const countsByStory = report.matches.reduce((acc, match) => {
|
|
41
|
+
const slug = match.storyFullSlug || match.storySlug || "[unknown]";
|
|
42
|
+
acc[slug] = (acc[slug] || 0) + 1;
|
|
43
|
+
return acc;
|
|
44
|
+
}, {});
|
|
45
|
+
if (Object.keys(countsByStory).length === 0) {
|
|
46
|
+
Logger.warning("No matches found.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
Logger.log("Matches by story:");
|
|
50
|
+
for (const [slug, count] of Object.entries(countsByStory)) {
|
|
51
|
+
Logger.log(`- ${slug}: ${count}`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
export const inspect = async (props) => {
|
|
55
|
+
const { input, flags } = props;
|
|
56
|
+
const command = input[1];
|
|
57
|
+
switch (command) {
|
|
58
|
+
case INSPECT_COMMANDS.componentUsage: {
|
|
59
|
+
const from = getFrom(flags, apiConfig);
|
|
60
|
+
const queryNameOrPath = flags["query"];
|
|
61
|
+
const withSlug = normalizeWithSlug(flags["withSlug"]);
|
|
62
|
+
const startsWith = flags["startsWith"] || undefined;
|
|
63
|
+
const all = Boolean(flags["all"]);
|
|
64
|
+
const outputPath = flags["outputPath"];
|
|
65
|
+
assertValidSelection({ all, withSlug, startsWith });
|
|
66
|
+
if (!queryNameOrPath) {
|
|
67
|
+
throw new Error("Missing query. Pass --query with a component usage query file name or path.");
|
|
68
|
+
}
|
|
69
|
+
const query = await loadComponentUsageQuery(queryNameOrPath);
|
|
70
|
+
const report = await inspectStoryblokStories({
|
|
71
|
+
query,
|
|
72
|
+
spaceId: from,
|
|
73
|
+
filters: {
|
|
74
|
+
all: all || undefined,
|
|
75
|
+
withSlug,
|
|
76
|
+
startsWith,
|
|
77
|
+
},
|
|
78
|
+
}, apiConfig);
|
|
79
|
+
printComponentUsageSummary(report);
|
|
80
|
+
if (outputPath) {
|
|
81
|
+
await createAndSaveToFile({
|
|
82
|
+
path: outputPath,
|
|
83
|
+
res: report,
|
|
84
|
+
}, apiConfig);
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
throw new Error("Unknown inspect command. Supported command: component-usage.");
|
|
90
|
+
}
|
|
91
|
+
};
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import meow from "meow";
|
|
3
|
-
import { backupDescription, debugDescription, mainDescription, syncDescription, removeDescription, initDescription, discoverDescription, migrateDescription, languagePublishStateDescription, storyVersionsDescription, publishedLayerExportDescription, revertDescription, migrationsDescription, copyDescription, } from "./cli-descriptions.js";
|
|
3
|
+
import { backupDescription, debugDescription, mainDescription, syncDescription, removeDescription, initDescription, discoverDescription, inspectDescription, migrateDescription, languagePublishStateDescription, storyVersionsDescription, publishedLayerExportDescription, revertDescription, migrationsDescription, copyDescription, } from "./cli-descriptions.js";
|
|
4
4
|
import { pipe, prop } from "./utils/cli-utils.js";
|
|
5
5
|
const app = () => ({
|
|
6
6
|
cli: meow(mainDescription, {
|
|
@@ -229,6 +229,38 @@ app.discover = () => ({
|
|
|
229
229
|
await discover(cli);
|
|
230
230
|
},
|
|
231
231
|
});
|
|
232
|
+
app.inspect = () => ({
|
|
233
|
+
cli: meow(inspectDescription, {
|
|
234
|
+
importMeta: import.meta,
|
|
235
|
+
booleanDefault: undefined,
|
|
236
|
+
flags: {
|
|
237
|
+
from: {
|
|
238
|
+
type: "string",
|
|
239
|
+
},
|
|
240
|
+
all: {
|
|
241
|
+
type: "boolean",
|
|
242
|
+
default: false,
|
|
243
|
+
},
|
|
244
|
+
withSlug: {
|
|
245
|
+
type: "string",
|
|
246
|
+
isMultiple: true,
|
|
247
|
+
},
|
|
248
|
+
startsWith: {
|
|
249
|
+
type: "string",
|
|
250
|
+
},
|
|
251
|
+
query: {
|
|
252
|
+
type: "string",
|
|
253
|
+
},
|
|
254
|
+
outputPath: {
|
|
255
|
+
type: "string",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
action: async (cli) => {
|
|
260
|
+
const { inspect } = await import("./commands/inspect.js");
|
|
261
|
+
await inspect(cli);
|
|
262
|
+
},
|
|
263
|
+
});
|
|
232
264
|
app.migrations = () => ({
|
|
233
265
|
cli: meow(migrationsDescription, {
|
|
234
266
|
importMeta: import.meta,
|