@youcan/theme 1.2.0-beta.7 → 1.2.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/theme/delete.d.ts +4 -0
- package/dist/cli/commands/theme/delete.js +32 -0
- package/dist/cli/commands/theme/dev.d.ts +4 -0
- package/dist/cli/commands/theme/dev.js +35 -0
- package/dist/cli/commands/theme/init.js +1 -7
- package/dist/cli/services/dev/worker.d.ts +17 -0
- package/dist/cli/services/dev/worker.js +138 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- package/dist/util/theme-loader.d.ts +2 -0
- package/dist/util/theme-loader.js +16 -0
- package/package.json +4 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Session, Http, Env, Tasks } from '@youcan/cli-kit';
|
|
2
|
+
import { ThemeCommand } from '../../../util/theme-command.js';
|
|
3
|
+
|
|
4
|
+
class Delete extends ThemeCommand {
|
|
5
|
+
async run() {
|
|
6
|
+
await Session.authenticate(this);
|
|
7
|
+
const { dev: themes } = await Http.get(`${Env.apiHostname()}/themes`);
|
|
8
|
+
if (!themes.length) {
|
|
9
|
+
return this.output.info('You have no remote dev themes');
|
|
10
|
+
}
|
|
11
|
+
const choices = themes.map(t => ({ title: t.name, value: t.id }));
|
|
12
|
+
const { identifiers } = await this.prompt({
|
|
13
|
+
choices,
|
|
14
|
+
type: 'multiselect',
|
|
15
|
+
name: 'identifiers',
|
|
16
|
+
message: 'Select themes to delete',
|
|
17
|
+
});
|
|
18
|
+
const tasks = identifiers.map((id) => {
|
|
19
|
+
const theme = themes.find(t => t.id === id);
|
|
20
|
+
return {
|
|
21
|
+
title: `Deleting '${theme.name}'...`,
|
|
22
|
+
async task() {
|
|
23
|
+
await Http.post(`${Env.apiHostname()}/themes/${theme.id}/delete`);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
await Tasks.run({}, tasks);
|
|
28
|
+
this.output.info(`Done! ${identifiers.length} themes deleted.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { Delete as default };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Session, Tasks, Http, Env } from '@youcan/cli-kit';
|
|
2
|
+
import { ThemeCommand } from '../../../util/theme-command.js';
|
|
3
|
+
import { load } from '../../../util/theme-loader.js';
|
|
4
|
+
import ThemeWorker from '../../services/dev/worker.js';
|
|
5
|
+
|
|
6
|
+
class Dev extends ThemeCommand {
|
|
7
|
+
async run() {
|
|
8
|
+
const theme = await load();
|
|
9
|
+
await Session.authenticate(this);
|
|
10
|
+
const context = await Tasks.run({ cmd: this, workers: [] }, [
|
|
11
|
+
{
|
|
12
|
+
title: 'Fetching theme info...',
|
|
13
|
+
async task(ctx) {
|
|
14
|
+
const res = await Http.get(`${Env.apiHostname()}/me`);
|
|
15
|
+
ctx.store = {
|
|
16
|
+
slug: res.slug,
|
|
17
|
+
domain: res.domain,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: 'Preparing dev processes...',
|
|
23
|
+
async task(ctx) {
|
|
24
|
+
ctx.workers = [
|
|
25
|
+
new ThemeWorker(ctx.cmd, ctx.store, theme),
|
|
26
|
+
];
|
|
27
|
+
await Promise.all(ctx.workers.map(async (w) => await w.boot()));
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
await Promise.all(context.workers.map(async (w) => await w.run()));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { Dev as default };
|
|
@@ -68,7 +68,7 @@ async function prompt(command) {
|
|
|
68
68
|
{
|
|
69
69
|
name: 'theme_name',
|
|
70
70
|
type: 'text',
|
|
71
|
-
initial: '
|
|
71
|
+
initial: 'Starter',
|
|
72
72
|
message: 'Your theme\'s name',
|
|
73
73
|
validate: (v) => {
|
|
74
74
|
if (!v.length) {
|
|
@@ -80,12 +80,6 @@ async function prompt(command) {
|
|
|
80
80
|
return true;
|
|
81
81
|
},
|
|
82
82
|
},
|
|
83
|
-
{
|
|
84
|
-
type: 'text',
|
|
85
|
-
name: 'theme_name',
|
|
86
|
-
message: 'The theme\'s name, used for display purposes.',
|
|
87
|
-
initial: 'Starter',
|
|
88
|
-
},
|
|
89
83
|
{
|
|
90
84
|
type: 'text',
|
|
91
85
|
name: 'theme_author',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Worker } from '@youcan/cli-kit';
|
|
2
|
+
import type { ThemeCommand } from '@/util/theme-command';
|
|
3
|
+
import type { Metadata, Store, Theme } from '@/types';
|
|
4
|
+
export default class ThemeWorker extends Worker.Abstract {
|
|
5
|
+
private command;
|
|
6
|
+
private store;
|
|
7
|
+
private theme;
|
|
8
|
+
private logger;
|
|
9
|
+
private previewLogger;
|
|
10
|
+
private queue;
|
|
11
|
+
private io;
|
|
12
|
+
FILE_TYPES: Array<keyof Metadata>;
|
|
13
|
+
constructor(command: ThemeCommand, store: Store, theme: Theme);
|
|
14
|
+
boot(): Promise<void>;
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
private enqueue;
|
|
17
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Worker, Color, Http, Env, System, Path, Filesystem, Crypto, Form } from '@youcan/cli-kit';
|
|
2
|
+
import { Server } from 'socket.io';
|
|
3
|
+
import debounce from 'debounce';
|
|
4
|
+
|
|
5
|
+
class ThemeWorker extends Worker.Abstract {
|
|
6
|
+
command;
|
|
7
|
+
store;
|
|
8
|
+
theme;
|
|
9
|
+
logger;
|
|
10
|
+
previewLogger;
|
|
11
|
+
queue = [];
|
|
12
|
+
io;
|
|
13
|
+
FILE_TYPES = [
|
|
14
|
+
'layouts',
|
|
15
|
+
'sections',
|
|
16
|
+
'locales',
|
|
17
|
+
'assets',
|
|
18
|
+
'snippets',
|
|
19
|
+
'config',
|
|
20
|
+
'templates',
|
|
21
|
+
];
|
|
22
|
+
constructor(command, store, theme) {
|
|
23
|
+
super();
|
|
24
|
+
this.command = command;
|
|
25
|
+
this.store = store;
|
|
26
|
+
this.theme = theme;
|
|
27
|
+
this.logger = new Worker.Logger('stdout', 'themes', Color.magenta);
|
|
28
|
+
this.previewLogger = new Worker.Logger('stdout', 'preview', Color.dim);
|
|
29
|
+
}
|
|
30
|
+
async boot() {
|
|
31
|
+
try {
|
|
32
|
+
const res = await Http.get(`${Env.apiHostname()}/themes/${this.theme.theme_id}/metadata`);
|
|
33
|
+
this.theme.metadata = res;
|
|
34
|
+
this.io = new Server(7565, {
|
|
35
|
+
cors: {
|
|
36
|
+
origin: `${Http.scheme()}://${this.store.domain}`,
|
|
37
|
+
methods: ['GET', 'POST'],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
this.io.on('connection', (socket) => {
|
|
41
|
+
this.previewLogger.write(`attached to preview page at ${socket.handshake.address}`);
|
|
42
|
+
});
|
|
43
|
+
System.open(`${Http.scheme()}://${this.store.domain}/themes/${this.theme.theme_id}/preview`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
this.command.error(err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async run() {
|
|
50
|
+
this.logger.write(`pushing changes to ${this.theme.metadata.theme_name}...`);
|
|
51
|
+
for (const type of this.FILE_TYPES) {
|
|
52
|
+
const descriptors = this.theme.metadata[type] ?? [];
|
|
53
|
+
const directory = Path.resolve(this.theme.root, type);
|
|
54
|
+
const present = await Filesystem.exists(directory)
|
|
55
|
+
? await Filesystem.readdir(directory)
|
|
56
|
+
: [];
|
|
57
|
+
if (type === 'config') {
|
|
58
|
+
const order = ['settings_schema.json', 'settings_data.json'];
|
|
59
|
+
descriptors.sort((a, b) => order.indexOf(a.file_name) - order.indexOf(b.file_name));
|
|
60
|
+
}
|
|
61
|
+
present.filter(f => !descriptors.find(d => d.file_name === f))
|
|
62
|
+
.forEach(async (file) => this.enqueue('save', type, file));
|
|
63
|
+
descriptors.forEach(async (descriptor) => {
|
|
64
|
+
const path = Path.resolve(directory, descriptor.file_name);
|
|
65
|
+
if (!(await Filesystem.exists(path))) {
|
|
66
|
+
return this.enqueue('delete', type, descriptor.file_name);
|
|
67
|
+
}
|
|
68
|
+
const buffer = await Filesystem.readFile(path);
|
|
69
|
+
const hash = Crypto.sha1(buffer);
|
|
70
|
+
if (hash !== descriptor.hash) {
|
|
71
|
+
this.enqueue('save', type, descriptor.file_name);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const directories = this.FILE_TYPES.map(t => Path.resolve(this.theme.root, t));
|
|
76
|
+
const watcher = Filesystem.watch(directories, {
|
|
77
|
+
awaitWriteFinish: { stabilityThreshold: 50 },
|
|
78
|
+
ignoreInitial: true,
|
|
79
|
+
persistent: true,
|
|
80
|
+
});
|
|
81
|
+
this.command.controller.signal.addEventListener('abort', () => {
|
|
82
|
+
watcher.close();
|
|
83
|
+
});
|
|
84
|
+
watcher.on('all', async (event, path) => {
|
|
85
|
+
if (!['add', 'change', 'unlink'].includes(event)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const [filetype, filename] = [
|
|
89
|
+
Path.basename(Path.dirname(path)),
|
|
90
|
+
Path.basename(path),
|
|
91
|
+
];
|
|
92
|
+
switch (event) {
|
|
93
|
+
case 'add':
|
|
94
|
+
case 'change':
|
|
95
|
+
this.enqueue('save', filetype, filename);
|
|
96
|
+
break;
|
|
97
|
+
case 'unlink':
|
|
98
|
+
this.enqueue('delete', filetype, filename);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// quick racing conditions hack
|
|
103
|
+
setInterval(async () => {
|
|
104
|
+
const task = this.queue.shift();
|
|
105
|
+
if (task == null) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await task();
|
|
109
|
+
}, 10);
|
|
110
|
+
}
|
|
111
|
+
enqueue(op, type, name) {
|
|
112
|
+
this.queue.push(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const path = Path.join(this.theme.root, type, name);
|
|
115
|
+
await Http.post(`${Env.apiHostname()}/themes/${this.theme.theme_id}/update`, {
|
|
116
|
+
body: Form.convert({
|
|
117
|
+
file_name: name,
|
|
118
|
+
file_type: type,
|
|
119
|
+
file_operation: op,
|
|
120
|
+
file_content: op === 'save'
|
|
121
|
+
? await Form.file(path)
|
|
122
|
+
: undefined,
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
this.logger.write(`[${op === 'save' ? 'updated' : 'deleted'}] - ${Path.join(type, name)}`);
|
|
126
|
+
debounce(() => {
|
|
127
|
+
this.io.emit('theme:update');
|
|
128
|
+
this.previewLogger.write('reloading preview...');
|
|
129
|
+
}, 100)();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
this.logger.write(`[error] - ${Path.join(type, name)}`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { ThemeWorker as default };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface FileDescriptor {
|
|
2
|
+
id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
name: string;
|
|
5
|
+
file_name: string;
|
|
6
|
+
updated: boolean;
|
|
7
|
+
deleted: boolean;
|
|
8
|
+
size: number;
|
|
9
|
+
hash: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Metadata {
|
|
12
|
+
theme_name: string;
|
|
13
|
+
theme_author: string;
|
|
14
|
+
theme_version: string;
|
|
15
|
+
theme_support_url: string;
|
|
16
|
+
theme_documentation_url: string;
|
|
17
|
+
config: FileDescriptor[];
|
|
18
|
+
layouts: FileDescriptor[];
|
|
19
|
+
sections: FileDescriptor[];
|
|
20
|
+
templates: FileDescriptor[];
|
|
21
|
+
locales: FileDescriptor[];
|
|
22
|
+
snippets: FileDescriptor[];
|
|
23
|
+
assets: FileDescriptor[];
|
|
24
|
+
}
|
|
25
|
+
export interface Theme {
|
|
26
|
+
root: string;
|
|
27
|
+
theme_id: string;
|
|
28
|
+
metadata?: Metadata;
|
|
29
|
+
}
|
|
30
|
+
export interface ThemeInfo {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
size: number;
|
|
34
|
+
version: string;
|
|
35
|
+
live: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface Store {
|
|
38
|
+
domain: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Path, Filesystem } from '@youcan/cli-kit';
|
|
2
|
+
import { THEME_CONFIG_FILENAME } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
async function load() {
|
|
5
|
+
const path = Path.resolve(Path.cwd(), THEME_CONFIG_FILENAME);
|
|
6
|
+
if (!await Filesystem.exists(path)) {
|
|
7
|
+
throw new Error(`Theme config not found at ${path}`);
|
|
8
|
+
}
|
|
9
|
+
const config = await Filesystem.readJsonFile(path);
|
|
10
|
+
return {
|
|
11
|
+
...config,
|
|
12
|
+
root: Path.cwd(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { load };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youcan/theme",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.0-beta.
|
|
4
|
+
"version": "1.2.0-beta.8",
|
|
5
5
|
"description": "OCLIF plugin for building themes",
|
|
6
6
|
"author": "YouCan <contact@youcan.shop> (https://youcan.shop)",
|
|
7
7
|
"license": "MIT",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@oclif/core": "^2.15.0",
|
|
19
|
-
"
|
|
19
|
+
"debounce": "^2.0.0",
|
|
20
|
+
"socket.io": "^4.7.2",
|
|
21
|
+
"@youcan/cli-kit": "1.2.0-beta.8"
|
|
20
22
|
},
|
|
21
23
|
"devDependencies": {
|
|
22
24
|
"@oclif/plugin-legacy": "^1.3.0",
|