@loopress/cli 0.7.0 → 0.9.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/README.md +44 -75
- package/dist/commands/composer/pull.d.ts +0 -3
- package/dist/commands/composer/pull.js +4 -15
- package/dist/commands/composer/push.d.ts +0 -3
- package/dist/commands/composer/push.js +7 -17
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/plugin/add.d.ts +0 -3
- package/dist/commands/plugin/add.js +9 -10
- package/dist/commands/plugin/pull.d.ts +0 -3
- package/dist/commands/plugin/pull.js +7 -19
- package/dist/commands/plugin/push.d.ts +4 -3
- package/dist/commands/plugin/push.js +69 -63
- package/dist/commands/sentry-test.d.ts +6 -0
- package/dist/commands/sentry-test.js +8 -0
- package/dist/commands/snippet/list.d.ts +1 -4
- package/dist/commands/snippet/list.js +24 -44
- package/dist/commands/snippet/pull.d.ts +1 -4
- package/dist/commands/snippet/pull.js +40 -56
- package/dist/commands/snippet/push.d.ts +2 -4
- package/dist/commands/snippet/push.js +90 -77
- package/dist/config/auth.manager.d.ts +0 -2
- package/dist/config/auth.manager.js +5 -25
- package/dist/config/json-file.d.ts +2 -0
- package/dist/config/json-file.js +21 -0
- package/dist/config/project-config.manager.d.ts +1 -3
- package/dist/config/project-config.manager.js +7 -23
- package/dist/hooks/finally.d.ts +3 -0
- package/dist/hooks/finally.js +21 -0
- package/dist/hooks/init.d.ts +3 -0
- package/dist/hooks/init.js +18 -0
- package/dist/lib/base.d.ts +13 -8
- package/dist/lib/base.js +45 -43
- package/dist/lib/push-command.d.ts +0 -1
- package/dist/lib/push-command.js +0 -1
- package/dist/lib/sentry.d.ts +8 -0
- package/dist/lib/sentry.js +24 -0
- package/dist/lib/wp-client.d.ts +15 -0
- package/dist/lib/wp-client.js +53 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/global-config.generated.d.ts +59 -0
- package/dist/types/global-config.generated.js +2 -0
- package/dist/types/project-config.generated.d.ts +31 -0
- package/dist/types/project-config.generated.js +2 -0
- package/dist/types/snippet.d.ts +7 -1
- package/dist/types/snippet.generated.d.ts +46 -0
- package/dist/types/snippet.generated.js +2 -0
- package/dist/utils/loopress-config.d.ts +2 -7
- package/dist/utils/loopress-config.js +5 -2
- package/dist/utils/snippet-plugin-flag.d.ts +3 -0
- package/dist/utils/snippet-plugin-flag.js +8 -0
- package/dist/utils/snippet-plugin.d.ts +23 -2
- package/dist/utils/snippet-plugin.js +168 -13
- package/oclif.manifest.json +66 -200
- package/package.json +19 -5
- package/dist/config/types.d.ts +0 -19
- package/dist/types/menu.d.ts +0 -7
- package/dist/types/menu.js +0 -1
- /package/dist/{config/types.js → types/config.js} +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { platform, release } from 'node:os';
|
|
2
|
+
// DSNs are write-only and safe to embed in a distributed CLI, see https://docs.sentry.io/product/security/#can-i-make-my-sentry-dsn-private
|
|
3
|
+
export const SENTRY_DSN = 'https://a08dd56bfffc2a45d5b8f665e4cb8b7d@o4511586904309760.ingest.de.sentry.io/4511673275973712';
|
|
4
|
+
export function consumeErrorReportingFlag(argv) {
|
|
5
|
+
const index = argv.indexOf('--no-error-reporting');
|
|
6
|
+
if (index === -1)
|
|
7
|
+
return;
|
|
8
|
+
argv.splice(index, 1);
|
|
9
|
+
process.env.LOOPRESS_TELEMETRY_DISABLED = '1';
|
|
10
|
+
}
|
|
11
|
+
export function isTelemetryDisabled() {
|
|
12
|
+
return process.env.LOOPRESS_TELEMETRY_DISABLED === '1';
|
|
13
|
+
}
|
|
14
|
+
export function resolveEnvironment() {
|
|
15
|
+
if (process.env.SENTRY_ENVIRONMENT)
|
|
16
|
+
return process.env.SENTRY_ENVIRONMENT;
|
|
17
|
+
return process.env.NODE_ENV === 'development' ? 'development' : 'production';
|
|
18
|
+
}
|
|
19
|
+
export function runtimeContext() {
|
|
20
|
+
return {
|
|
21
|
+
node: process.version,
|
|
22
|
+
os: `${platform()} ${release()}`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client for a WordPress site's REST API.
|
|
4
|
+
* Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/plugins`.
|
|
5
|
+
*/
|
|
6
|
+
export declare class WpClient {
|
|
7
|
+
private readonly siteUrl;
|
|
8
|
+
private readonly client;
|
|
9
|
+
constructor(siteUrl: string, token: string);
|
|
10
|
+
get<T>(path: string): Promise<T>;
|
|
11
|
+
post<T = unknown>(path: string, json?: Record<string, unknown>): Promise<T>;
|
|
12
|
+
put<T = unknown>(path: string, json?: Record<string, unknown>): Promise<T>;
|
|
13
|
+
private request;
|
|
14
|
+
}
|
|
15
|
+
export declare function formatWpError(error: unknown, url: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import got from 'got';
|
|
2
|
+
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
3
|
+
/**
|
|
4
|
+
* HTTP client for a WordPress site's REST API.
|
|
5
|
+
* Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/plugins`.
|
|
6
|
+
*/
|
|
7
|
+
export class WpClient {
|
|
8
|
+
siteUrl;
|
|
9
|
+
client;
|
|
10
|
+
constructor(siteUrl, token) {
|
|
11
|
+
this.siteUrl = siteUrl;
|
|
12
|
+
this.client = got.extend({
|
|
13
|
+
headers: { Authorization: `Basic ${Buffer.from(token).toString('base64')}` },
|
|
14
|
+
prefixUrl: `${siteUrl}/wp-json`,
|
|
15
|
+
timeout: { request: REQUEST_TIMEOUT_MS },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async get(path) {
|
|
19
|
+
return this.request('get', path);
|
|
20
|
+
}
|
|
21
|
+
async post(path, json) {
|
|
22
|
+
return this.request('post', path, json);
|
|
23
|
+
}
|
|
24
|
+
async put(path, json) {
|
|
25
|
+
return this.request('put', path, json);
|
|
26
|
+
}
|
|
27
|
+
async request(method, path, json) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await this.client(path, { json, method });
|
|
30
|
+
return (response.body ? JSON.parse(response.body) : undefined);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(formatWpError(error, `${this.siteUrl}/wp-json/${path}`), { cause: error });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function formatWpError(error, url) {
|
|
38
|
+
const err = error;
|
|
39
|
+
const status = err.response?.statusCode;
|
|
40
|
+
if (status === 401 || status === 403) {
|
|
41
|
+
return `Authentication failed (${status}) on ${url}. Check your credentials with \`lps project config\`.`;
|
|
42
|
+
}
|
|
43
|
+
if (status === 404) {
|
|
44
|
+
return `Endpoint not found (404) on ${url}. Is the required plugin installed and up to date on the site?`;
|
|
45
|
+
}
|
|
46
|
+
if (status !== undefined) {
|
|
47
|
+
return `Request failed (${status}) on ${url}.`;
|
|
48
|
+
}
|
|
49
|
+
if (err.name === 'TimeoutError') {
|
|
50
|
+
return `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s on ${url}. Is the site reachable?`;
|
|
51
|
+
}
|
|
52
|
+
return `Request to ${url} failed: ${err.message ?? String(error)}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { CurrentProjectPointer, EnvironmentConfig, LoopressGlobalConfiguration as LoopressConfig, ProjectConfig, } from './global-config.generated.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global CLI state stored at ~/.loopress/config.json: known projects, their environments, and which one is currently active.
|
|
3
|
+
*/
|
|
4
|
+
export interface LoopressGlobalConfiguration {
|
|
5
|
+
/**
|
|
6
|
+
* Pointer to the currently active project and environment, or null if none is selected.
|
|
7
|
+
*/
|
|
8
|
+
currentProject: null | CurrentProjectPointer;
|
|
9
|
+
/**
|
|
10
|
+
* Known projects, keyed by project id.
|
|
11
|
+
*/
|
|
12
|
+
projects: {
|
|
13
|
+
[k: string]: ProjectConfig;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface CurrentProjectPointer {
|
|
17
|
+
/**
|
|
18
|
+
* Name of the currently active environment.
|
|
19
|
+
*/
|
|
20
|
+
env: string;
|
|
21
|
+
/**
|
|
22
|
+
* Id of the currently active project.
|
|
23
|
+
*/
|
|
24
|
+
id: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ProjectConfig {
|
|
27
|
+
/**
|
|
28
|
+
* ISO timestamp of when the project was added.
|
|
29
|
+
*/
|
|
30
|
+
addedAt: string;
|
|
31
|
+
/**
|
|
32
|
+
* Environments for this project, keyed by environment name.
|
|
33
|
+
*/
|
|
34
|
+
environments: {
|
|
35
|
+
[k: string]: EnvironmentConfig;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Human-readable project name.
|
|
39
|
+
*/
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
export interface EnvironmentConfig {
|
|
43
|
+
/**
|
|
44
|
+
* ISO timestamp of when the environment was added.
|
|
45
|
+
*/
|
|
46
|
+
addedAt: string;
|
|
47
|
+
/**
|
|
48
|
+
* Environment name (e.g. "production", "staging").
|
|
49
|
+
*/
|
|
50
|
+
name: string;
|
|
51
|
+
/**
|
|
52
|
+
* API token used to authenticate against this environment.
|
|
53
|
+
*/
|
|
54
|
+
token?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Base URL of the WordPress site for this environment.
|
|
57
|
+
*/
|
|
58
|
+
url: string;
|
|
59
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-level config for the Loopress CLI (loopress.json).
|
|
3
|
+
*/
|
|
4
|
+
export interface LoopressProjectConfiguration {
|
|
5
|
+
$schema?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Base directory for all Loopress paths. All other path options are resolved relative to this directory.
|
|
8
|
+
*/
|
|
9
|
+
rootDir?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Directory where code snippets are read from and written to, relative to rootDir.
|
|
12
|
+
*/
|
|
13
|
+
snippetsDir?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Project identifier from the global Loopress config. When set, takes precedence over the currently active project in the global config.
|
|
16
|
+
*/
|
|
17
|
+
projectId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* WordPress snippet plugin to use for pull and push commands.
|
|
20
|
+
*/
|
|
21
|
+
snippetPlugin?: 'wpcode' | 'code-snippets';
|
|
22
|
+
/**
|
|
23
|
+
* Pinned plugin versions. Keys are WordPress.org plugin slugs, values are version constraints.
|
|
24
|
+
*/
|
|
25
|
+
plugins?: {
|
|
26
|
+
/**
|
|
27
|
+
* Version to pin. Use an exact version (e.g. "8.9.1") or "latest".
|
|
28
|
+
*/
|
|
29
|
+
[k: string]: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/types/snippet.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { SnippetType } from '../utils/snippet-plugin.js';
|
|
1
|
+
import { SnippetInsertMethod, SnippetLocation, SnippetType } from '../utils/snippet-plugin.js';
|
|
2
2
|
export interface Snippet {
|
|
3
|
+
active: boolean;
|
|
3
4
|
code: string;
|
|
4
5
|
id?: number;
|
|
6
|
+
insertMethod: SnippetInsertMethod;
|
|
7
|
+
location: SnippetLocation;
|
|
5
8
|
name: string;
|
|
6
9
|
path: string;
|
|
10
|
+
priority: number;
|
|
11
|
+
shortcodeAttributes: string[];
|
|
12
|
+
tags: string[];
|
|
7
13
|
type: SnippetType;
|
|
8
14
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidecar .json file paired with a snippet's code file in the snippets directory (e.g. `123-my-snippet.php` + `123-my-snippet.json`).
|
|
3
|
+
*/
|
|
4
|
+
export interface LoopressSnippetMetadata {
|
|
5
|
+
$schema?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Remote snippet id. Omitted until the snippet has been pushed for the first time.
|
|
8
|
+
*/
|
|
9
|
+
id?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Snippet name/title. Defaults to the code file's name when omitted.
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Snippet language. Defaults to the type inferred from the code file's extension when omitted.
|
|
16
|
+
*/
|
|
17
|
+
type?: 'css' | 'html' | 'js' | 'php' | 'text';
|
|
18
|
+
/**
|
|
19
|
+
* Whether the snippet is enabled.
|
|
20
|
+
*/
|
|
21
|
+
active?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Where the snippet runs. Supported values depend on the target plugin and snippet type.
|
|
24
|
+
*/
|
|
25
|
+
location?: 'admin' | 'body' | 'everywhere' | 'footer' | 'frontend' | 'header' | 'once';
|
|
26
|
+
/**
|
|
27
|
+
* Free-text note about the snippet.
|
|
28
|
+
*/
|
|
29
|
+
description?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Tags for organizing snippets.
|
|
32
|
+
*/
|
|
33
|
+
tags?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* How the snippet is inserted into the page.
|
|
36
|
+
*/
|
|
37
|
+
insertMethod?: 'auto' | 'shortcode';
|
|
38
|
+
/**
|
|
39
|
+
* Execution priority. Lower runs earlier.
|
|
40
|
+
*/
|
|
41
|
+
priority?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Attribute names accepted by the snippet's shortcode, used when insertMethod is "shortcode".
|
|
44
|
+
*/
|
|
45
|
+
shortcodeAttributes?: string[];
|
|
46
|
+
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
projectId?: string;
|
|
4
|
-
rootDir?: string;
|
|
5
|
-
snippetPlugin?: 'code-snippets' | 'wpcode';
|
|
6
|
-
snippetsDir?: string;
|
|
7
|
-
}
|
|
1
|
+
import { LoopressProjectConfiguration } from '../types/project-config.generated.js';
|
|
2
|
+
export type LoopressLocalConfig = LoopressProjectConfiguration;
|
|
8
3
|
export declare function readLocalConfig(): Promise<LoopressLocalConfig>;
|
|
9
4
|
export declare function writeLocalConfig(config: LoopressLocalConfig): Promise<void>;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
// Only a missing file is treated as "no config" (returns {}). A file that exists but
|
|
5
|
+
// fails to read or parse throws, so callers don't silently fall back to the global
|
|
6
|
+
// current environment when loopress.json is actually broken.
|
|
4
7
|
export async function readLocalConfig() {
|
|
5
8
|
const configPath = join(process.cwd(), 'loopress.json');
|
|
6
9
|
if (!existsSync(configPath))
|
|
7
10
|
return {};
|
|
11
|
+
const content = await readFile(configPath, 'utf8');
|
|
8
12
|
try {
|
|
9
|
-
const content = await readFile(configPath, 'utf8');
|
|
10
13
|
return JSON.parse(content);
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
13
|
-
|
|
16
|
+
throw new Error('loopress.json is not valid JSON. Fix or delete it, then run `lps init` again.');
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
export async function writeLocalConfig(config) {
|
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
export type PluginName = 'code-snippets' | 'wpcode';
|
|
2
2
|
export type SnippetType = 'css' | 'html' | 'js' | 'php' | 'text';
|
|
3
|
+
export type SnippetInsertMethod = 'auto' | 'shortcode';
|
|
4
|
+
export type SnippetLocation = 'admin' | 'body' | 'everywhere' | 'footer' | 'frontend' | 'header' | 'once';
|
|
3
5
|
export interface NormalizedSnippet {
|
|
4
6
|
active: boolean;
|
|
5
7
|
code: string;
|
|
6
8
|
description: string;
|
|
7
9
|
id: number;
|
|
10
|
+
insertMethod: SnippetInsertMethod;
|
|
11
|
+
location: SnippetLocation;
|
|
8
12
|
name: string;
|
|
13
|
+
priority: number;
|
|
14
|
+
shortcodeAttributes: string[];
|
|
9
15
|
tags: string[];
|
|
10
16
|
type: SnippetType;
|
|
11
17
|
}
|
|
12
18
|
export declare function parseType(raw: unknown): null | SnippetType;
|
|
19
|
+
export declare function parseLocation(raw: unknown): null | SnippetLocation;
|
|
20
|
+
export declare function parseInsertMethod(raw: unknown): null | SnippetInsertMethod;
|
|
21
|
+
export declare function defaultLocationForType(type: SnippetType): SnippetLocation;
|
|
22
|
+
export interface SnippetPayloadInput {
|
|
23
|
+
active: boolean;
|
|
24
|
+
code: string;
|
|
25
|
+
insertMethod: SnippetInsertMethod;
|
|
26
|
+
location: SnippetLocation;
|
|
27
|
+
name: string;
|
|
28
|
+
path: string;
|
|
29
|
+
priority: number;
|
|
30
|
+
shortcodeAttributes: string[];
|
|
31
|
+
tags: string[];
|
|
32
|
+
type: SnippetType;
|
|
33
|
+
}
|
|
13
34
|
export interface SnippetPlugin {
|
|
14
|
-
|
|
35
|
+
endpointPath(): string;
|
|
15
36
|
fromRemote(data: Record<string, unknown>): NormalizedSnippet;
|
|
16
|
-
toPayload(
|
|
37
|
+
toPayload(snippet: SnippetPayloadInput): Record<string, unknown>;
|
|
17
38
|
}
|
|
18
39
|
export declare function getSnippetPlugin(name: PluginName): SnippetPlugin;
|
|
@@ -3,6 +3,30 @@ export function parseType(raw) {
|
|
|
3
3
|
const value = String(raw ?? '').toLowerCase();
|
|
4
4
|
return valid.includes(value) ? value : null;
|
|
5
5
|
}
|
|
6
|
+
const VALID_LOCATIONS = new Set(['admin', 'body', 'everywhere', 'footer', 'frontend', 'header', 'once']);
|
|
7
|
+
export function parseLocation(raw) {
|
|
8
|
+
const value = String(raw ?? '').toLowerCase();
|
|
9
|
+
return VALID_LOCATIONS.has(value) ? value : null;
|
|
10
|
+
}
|
|
11
|
+
export function parseInsertMethod(raw) {
|
|
12
|
+
return raw === 'auto' || raw === 'shortcode' ? raw : null;
|
|
13
|
+
}
|
|
14
|
+
// The sensible default placement for a freshly pushed snippet that doesn't specify a location.
|
|
15
|
+
export function defaultLocationForType(type) {
|
|
16
|
+
switch (type) {
|
|
17
|
+
case 'css': {
|
|
18
|
+
return 'header';
|
|
19
|
+
}
|
|
20
|
+
case 'html':
|
|
21
|
+
case 'js':
|
|
22
|
+
case 'text': {
|
|
23
|
+
return 'footer';
|
|
24
|
+
}
|
|
25
|
+
case 'php': {
|
|
26
|
+
return 'everywhere';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
6
30
|
function inferTypeFromCode(code) {
|
|
7
31
|
const firstLine = code.trimStart().split('\n')[0].trimStart();
|
|
8
32
|
if (firstLine.startsWith('<?'))
|
|
@@ -14,53 +38,184 @@ function inferTypeFromCode(code) {
|
|
|
14
38
|
function resolveType(raw, code) {
|
|
15
39
|
return parseType(raw) ?? inferTypeFromCode(code);
|
|
16
40
|
}
|
|
41
|
+
function resolvePriority(raw) {
|
|
42
|
+
const value = Number(raw);
|
|
43
|
+
return Number.isFinite(value) ? value : 10;
|
|
44
|
+
}
|
|
45
|
+
// Snippet files on disk keep the <?php opening tag so they're valid, syntax-highlighted PHP files.
|
|
46
|
+
// Both plugins store just the executable body, so it's stripped before push and restored on pull
|
|
47
|
+
// (see buildSnippetFile in commands/snippet/pull.ts).
|
|
48
|
+
function stripPhpOpeningTag(code) {
|
|
49
|
+
return code.replace(/^<\?php\s*/i, '');
|
|
50
|
+
}
|
|
51
|
+
// The real Code Snippets plugin has no independent "type" field: its REST API only has `scope`,
|
|
52
|
+
// and the snippet type is derived from it (see WPCode_Snippet::get_type_from_scope() upstream).
|
|
53
|
+
// Sending a `type` key (as this adapter used to) is silently ignored by that plugin.
|
|
54
|
+
const CODE_SNIPPETS_SCOPE_TO_LOCATION = {
|
|
55
|
+
admin: 'admin',
|
|
56
|
+
'admin-css': 'admin',
|
|
57
|
+
content: 'everywhere',
|
|
58
|
+
'footer-content': 'footer',
|
|
59
|
+
'front-end': 'frontend',
|
|
60
|
+
global: 'everywhere',
|
|
61
|
+
'head-content': 'header',
|
|
62
|
+
'single-use': 'once',
|
|
63
|
+
'site-css': 'frontend',
|
|
64
|
+
'site-footer-js': 'footer',
|
|
65
|
+
'site-head-js': 'header',
|
|
66
|
+
};
|
|
67
|
+
function typeFromScope(scope) {
|
|
68
|
+
if (scope.endsWith('-css'))
|
|
69
|
+
return 'css';
|
|
70
|
+
if (scope.endsWith('-js'))
|
|
71
|
+
return 'js';
|
|
72
|
+
if (scope.endsWith('content'))
|
|
73
|
+
return 'html';
|
|
74
|
+
return 'php';
|
|
75
|
+
}
|
|
76
|
+
function scopeFromTypeAndLocation(type, location) {
|
|
77
|
+
switch (type) {
|
|
78
|
+
case 'css': {
|
|
79
|
+
if (location === 'frontend')
|
|
80
|
+
return 'site-css';
|
|
81
|
+
if (location === 'admin')
|
|
82
|
+
return 'admin-css';
|
|
83
|
+
throw new Error(`Code Snippets does not support the "${location}" location for CSS snippets. Use one of: frontend, admin.`);
|
|
84
|
+
}
|
|
85
|
+
case 'html': {
|
|
86
|
+
if (location === 'header')
|
|
87
|
+
return 'head-content';
|
|
88
|
+
if (location === 'footer')
|
|
89
|
+
return 'footer-content';
|
|
90
|
+
if (location === 'everywhere')
|
|
91
|
+
return 'content';
|
|
92
|
+
throw new Error(`Code Snippets does not support the "${location}" location for HTML snippets. Use one of: header, footer, everywhere.`);
|
|
93
|
+
}
|
|
94
|
+
case 'js': {
|
|
95
|
+
if (location === 'header')
|
|
96
|
+
return 'site-head-js';
|
|
97
|
+
if (location === 'footer')
|
|
98
|
+
return 'site-footer-js';
|
|
99
|
+
throw new Error(`Code Snippets does not support the "${location}" location for JS snippets. Use one of: header, footer.`);
|
|
100
|
+
}
|
|
101
|
+
case 'php': {
|
|
102
|
+
if (location === 'everywhere')
|
|
103
|
+
return 'global';
|
|
104
|
+
if (location === 'frontend')
|
|
105
|
+
return 'front-end';
|
|
106
|
+
if (location === 'admin')
|
|
107
|
+
return 'admin';
|
|
108
|
+
if (location === 'once')
|
|
109
|
+
return 'single-use';
|
|
110
|
+
throw new Error(`Code Snippets does not support the "${location}" location for PHP snippets. Use one of: everywhere, frontend, admin, once.`);
|
|
111
|
+
}
|
|
112
|
+
case 'text': {
|
|
113
|
+
throw new Error('Code Snippets has no "text" snippet type. Change the sidecar "type", or push this snippet to "wpcode" instead.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
17
117
|
class CodeSnippetsPlugin {
|
|
18
|
-
|
|
19
|
-
return
|
|
118
|
+
endpointPath() {
|
|
119
|
+
return 'code-snippets/v1/snippets';
|
|
20
120
|
}
|
|
21
121
|
fromRemote(data) {
|
|
122
|
+
const scope = String(data.scope ?? 'global');
|
|
123
|
+
const type = typeFromScope(scope);
|
|
22
124
|
return {
|
|
23
125
|
active: Boolean(data.active),
|
|
24
126
|
code: String(data.code ?? ''),
|
|
25
127
|
description: String(data.desc ?? ''),
|
|
26
128
|
id: Number(data.id),
|
|
129
|
+
insertMethod: 'auto',
|
|
130
|
+
location: CODE_SNIPPETS_SCOPE_TO_LOCATION[scope] ?? defaultLocationForType(type),
|
|
27
131
|
name: String(data.name ?? ''),
|
|
132
|
+
priority: resolvePriority(data.priority),
|
|
133
|
+
shortcodeAttributes: [],
|
|
28
134
|
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
29
|
-
type
|
|
135
|
+
type,
|
|
30
136
|
};
|
|
31
137
|
}
|
|
32
|
-
toPayload(
|
|
138
|
+
toPayload({ active, code, location, name, path, priority, tags, type }) {
|
|
33
139
|
return {
|
|
34
|
-
|
|
140
|
+
active,
|
|
141
|
+
code: stripPhpOpeningTag(code),
|
|
35
142
|
desc: `Imported from ${path}`,
|
|
36
143
|
name,
|
|
37
|
-
|
|
38
|
-
type,
|
|
144
|
+
priority,
|
|
145
|
+
scope: scopeFromTypeAndLocation(type, location),
|
|
146
|
+
tags,
|
|
39
147
|
};
|
|
40
148
|
}
|
|
41
149
|
}
|
|
150
|
+
// WPCode taxonomy term slugs (see wpcode_register_taxonomies() and the auto-insert location
|
|
151
|
+
// classes upstream). 'everywhere' | 'frontend_only' | 'admin_only' | 'on_demand' only apply to
|
|
152
|
+
// PHP snippets; 'site_wide_header' | 'site_wide_body' | 'site_wide_footer' apply to any type.
|
|
153
|
+
const WPCODE_PHP_ONLY_LOCATIONS = {
|
|
154
|
+
admin: 'admin_only',
|
|
155
|
+
everywhere: 'everywhere',
|
|
156
|
+
frontend: 'frontend_only',
|
|
157
|
+
once: 'on_demand',
|
|
158
|
+
};
|
|
159
|
+
const WPCODE_UNIVERSAL_LOCATIONS = {
|
|
160
|
+
body: 'site_wide_body',
|
|
161
|
+
footer: 'site_wide_footer',
|
|
162
|
+
header: 'site_wide_header',
|
|
163
|
+
};
|
|
164
|
+
const WPCODE_LOCATION_TO_CANONICAL = {
|
|
165
|
+
'admin_only': 'admin',
|
|
166
|
+
everywhere: 'everywhere',
|
|
167
|
+
'frontend_only': 'frontend',
|
|
168
|
+
'on_demand': 'once',
|
|
169
|
+
'site_wide_body': 'body',
|
|
170
|
+
'site_wide_footer': 'footer',
|
|
171
|
+
'site_wide_header': 'header',
|
|
172
|
+
};
|
|
173
|
+
function wpcodeLocationTerm(type, location) {
|
|
174
|
+
const universal = WPCODE_UNIVERSAL_LOCATIONS[location];
|
|
175
|
+
if (universal)
|
|
176
|
+
return universal;
|
|
177
|
+
if (type === 'php') {
|
|
178
|
+
const phpOnly = WPCODE_PHP_ONLY_LOCATIONS[location];
|
|
179
|
+
if (phpOnly)
|
|
180
|
+
return phpOnly;
|
|
181
|
+
}
|
|
182
|
+
const allowed = type === 'php' ? 'header, body, footer, everywhere, frontend, admin, once' : 'header, body, footer';
|
|
183
|
+
throw new Error(`WPCode does not support the "${location}" location for ${type} snippets. Use one of: ${allowed}.`);
|
|
184
|
+
}
|
|
42
185
|
class WPCodePlugin {
|
|
43
|
-
|
|
44
|
-
return
|
|
186
|
+
endpointPath() {
|
|
187
|
+
return 'loopress/v1/wpcode/snippets';
|
|
45
188
|
}
|
|
46
189
|
fromRemote(data) {
|
|
190
|
+
const type = resolveType(data.type, String(data.code ?? ''));
|
|
47
191
|
return {
|
|
48
192
|
active: Boolean(data.active),
|
|
49
193
|
code: String(data.code ?? ''),
|
|
50
194
|
description: String(data.note ?? ''),
|
|
51
195
|
id: Number(data.id),
|
|
196
|
+
insertMethod: data.insert_method === 'shortcode' ? 'shortcode' : 'auto',
|
|
197
|
+
location: WPCODE_LOCATION_TO_CANONICAL[String(data.location)] ?? defaultLocationForType(type),
|
|
52
198
|
name: String(data.title ?? ''),
|
|
199
|
+
priority: resolvePriority(data.priority),
|
|
200
|
+
shortcodeAttributes: Array.isArray(data.shortcode_attributes) ? data.shortcode_attributes.map(String) : [],
|
|
53
201
|
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
54
|
-
type
|
|
202
|
+
type,
|
|
55
203
|
};
|
|
56
204
|
}
|
|
57
|
-
toPayload(
|
|
205
|
+
toPayload({ active, code, insertMethod, location, name, path, priority, shortcodeAttributes, tags, type, }) {
|
|
206
|
+
const placement = insertMethod === 'shortcode'
|
|
207
|
+
? { 'shortcode_attributes': shortcodeAttributes }
|
|
208
|
+
: { location: wpcodeLocationTerm(type, location) };
|
|
58
209
|
return {
|
|
59
|
-
|
|
210
|
+
active,
|
|
211
|
+
code: stripPhpOpeningTag(code),
|
|
60
212
|
note: `Imported from ${path}`,
|
|
61
|
-
|
|
213
|
+
priority,
|
|
214
|
+
tags,
|
|
62
215
|
title: name,
|
|
63
216
|
type,
|
|
217
|
+
...placement,
|
|
218
|
+
'insert_method': insertMethod,
|
|
64
219
|
};
|
|
65
220
|
}
|
|
66
221
|
}
|