@youcan/theme 2.0.5 → 2.1.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.
@@ -1,7 +1,9 @@
1
- import { Session, Tasks, Http, Env } from '@youcan/cli-kit';
1
+ import { Session, Tasks, Http, Env, Path, Filesystem, Crypto } from '@youcan/cli-kit';
2
2
  import { ThemeCommand } from '../../../util/theme-command.js';
3
3
  import { load } from '../../../util/theme-loader.js';
4
4
  import ThemeWorker from '../../services/dev/worker.js';
5
+ import { execute } from '../../services/dev/execute.js';
6
+ import { THEME_FILE_TYPES } from '../../../constants.js';
5
7
 
6
8
  class Dev extends ThemeCommand {
7
9
  static description = 'Start a theme development server and preview your changes';
@@ -10,13 +12,45 @@ class Dev extends ThemeCommand {
10
12
  await Session.authenticate(this);
11
13
  const context = await Tasks.run({ cmd: this, workers: [] }, [
12
14
  {
13
- title: 'Fetching theme info...',
15
+ title: 'Fetching theme metadata...',
14
16
  async task(ctx) {
15
- const res = await Http.get(`${Env.apiHostname()}/me`);
17
+ const store = await Http.get(`${Env.apiHostname()}/me`);
16
18
  ctx.store = {
17
- slug: res.slug,
18
- domain: res.domain,
19
+ slug: store.slug,
20
+ domain: store.domain,
19
21
  };
22
+ const metadata = await Http.get(`${Env.apiHostname()}/themes/${theme.theme_id}/metadata`);
23
+ theme.metadata = metadata;
24
+ },
25
+ },
26
+ {
27
+ title: 'Syncing theme files, please wait...',
28
+ async task(ctx) {
29
+ for (const type of THEME_FILE_TYPES) {
30
+ const descriptors = theme.metadata[type] ?? [];
31
+ const directory = Path.resolve(theme.root, type);
32
+ const present = await Filesystem.exists(directory)
33
+ ? await Filesystem.readdir(directory)
34
+ : [];
35
+ if (type === 'config') {
36
+ const order = ['settings_schema.json', 'settings_data.json'];
37
+ descriptors.sort((a, b) => order.indexOf(a.file_name) - order.indexOf(b.file_name));
38
+ }
39
+ for (const file of present.filter(f => !descriptors.find(d => d.file_name === f))) {
40
+ await execute(theme, 'save', type, file);
41
+ }
42
+ for (const descriptor of descriptors) {
43
+ const path = Path.resolve(directory, descriptor.file_name);
44
+ if (!(await Filesystem.exists(path))) {
45
+ return await execute(theme, 'delete', type, descriptor.file_name);
46
+ }
47
+ const buffer = await Filesystem.readFile(path);
48
+ const hash = Crypto.sha1(buffer);
49
+ if (hash !== descriptor.hash) {
50
+ await execute(theme, 'save', type, descriptor.file_name);
51
+ }
52
+ }
53
+ }
20
54
  },
21
55
  },
22
56
  {
@@ -0,0 +1,4 @@
1
+ import type { Worker } from '@youcan/cli-kit';
2
+ import type { Theme } from '@/types';
3
+ import type { THEME_FILE_TYPES } from '@/constants';
4
+ export declare function execute(theme: Theme, op: 'save' | 'delete', type: typeof THEME_FILE_TYPES[number], name: string, logger?: Worker.Logger | null): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { Path, Form, Http, Env } from '@youcan/cli-kit';
2
+
3
+ async function execute(theme, op, type, name, logger = null) {
4
+ try {
5
+ const path = Path.join(theme.root, type, name);
6
+ const payload = {
7
+ file_name: name,
8
+ file_type: type,
9
+ file_operation: op,
10
+ };
11
+ if (op === 'save') {
12
+ payload.file_content = await Form.file(path);
13
+ }
14
+ await Http.post(`${Env.apiHostname()}/themes/${theme.theme_id}/update`, {
15
+ body: Form.convert(payload),
16
+ });
17
+ logger && logger.write(`[${op === 'save' ? 'updated' : 'deleted'}] - ${Path.join(type, name)}`);
18
+ }
19
+ catch (err) {
20
+ if (err instanceof Error && logger) {
21
+ logger.write(`[error] - ${Path.join(type, name)}\n${err.message}`);
22
+ return;
23
+ }
24
+ throw err;
25
+ }
26
+ }
27
+
28
+ export { execute };
@@ -1,6 +1,6 @@
1
1
  import { Worker } from '@youcan/cli-kit';
2
2
  import type { ThemeCommand } from '@/util/theme-command';
3
- import type { Metadata, Store, Theme } from '@/types';
3
+ import type { Store, Theme } from '@/types';
4
4
  export default class ThemeWorker extends Worker.Abstract {
5
5
  private command;
6
6
  private store;
@@ -9,9 +9,9 @@ export default class ThemeWorker extends Worker.Abstract {
9
9
  private previewLogger;
10
10
  private queue;
11
11
  private io;
12
- FILE_TYPES: Array<keyof Metadata>;
13
12
  constructor(command: ThemeCommand, store: Store, theme: Theme);
14
13
  boot(): Promise<void>;
15
14
  run(): Promise<void>;
16
15
  private enqueue;
16
+ private execute;
17
17
  }
@@ -1,6 +1,8 @@
1
- import { Worker, Color, Http, Env, Path, Filesystem, Crypto, System, Form } from '@youcan/cli-kit';
1
+ import { Worker, Color, Http, System, Path, Filesystem } from '@youcan/cli-kit';
2
2
  import { Server } from 'socket.io';
3
3
  import debounce from 'debounce';
4
+ import { execute } from './execute.js';
5
+ import { THEME_FILE_TYPES } from '../../../constants.js';
4
6
 
5
7
  class ThemeWorker extends Worker.Abstract {
6
8
  command;
@@ -10,15 +12,6 @@ class ThemeWorker extends Worker.Abstract {
10
12
  previewLogger;
11
13
  queue = [];
12
14
  io;
13
- FILE_TYPES = [
14
- 'layouts',
15
- 'sections',
16
- 'locales',
17
- 'assets',
18
- 'snippets',
19
- 'config',
20
- 'templates',
21
- ];
22
15
  constructor(command, store, theme) {
23
16
  super();
24
17
  this.command = command;
@@ -29,33 +22,6 @@ class ThemeWorker extends Worker.Abstract {
29
22
  }
30
23
  async boot() {
31
24
  try {
32
- const res = await Http.get(`${Env.apiHostname()}/themes/${this.theme.theme_id}/metadata`);
33
- this.theme.metadata = res;
34
- this.logger.write(`pushing changes to ${this.theme.metadata.theme_name}...`);
35
- for (const type of this.FILE_TYPES) {
36
- const descriptors = this.theme.metadata[type] ?? [];
37
- const directory = Path.resolve(this.theme.root, type);
38
- const present = await Filesystem.exists(directory)
39
- ? await Filesystem.readdir(directory)
40
- : [];
41
- if (type === 'config') {
42
- const order = ['settings_schema.json', 'settings_data.json'];
43
- descriptors.sort((a, b) => order.indexOf(a.file_name) - order.indexOf(b.file_name));
44
- }
45
- present.filter(f => !descriptors.find(d => d.file_name === f))
46
- .forEach(async (file) => this.enqueue('save', type, file));
47
- descriptors.forEach(async (descriptor) => {
48
- const path = Path.resolve(directory, descriptor.file_name);
49
- if (!(await Filesystem.exists(path))) {
50
- return this.enqueue('delete', type, descriptor.file_name);
51
- }
52
- const buffer = await Filesystem.readFile(path);
53
- const hash = Crypto.sha1(buffer);
54
- if (hash !== descriptor.hash) {
55
- this.enqueue('save', type, descriptor.file_name);
56
- }
57
- });
58
- }
59
25
  this.io = new Server(7565, {
60
26
  cors: {
61
27
  origin: `${Http.scheme()}://${this.store.domain}`,
@@ -72,7 +38,7 @@ class ThemeWorker extends Worker.Abstract {
72
38
  }
73
39
  }
74
40
  async run() {
75
- const directories = this.FILE_TYPES.map(t => Path.resolve(this.theme.root, t));
41
+ const directories = THEME_FILE_TYPES.map(t => Path.resolve(this.theme.root, t));
76
42
  const watcher = Filesystem.watch(directories, {
77
43
  awaitWriteFinish: { stabilityThreshold: 50 },
78
44
  ignoreInitial: true,
@@ -99,6 +65,7 @@ class ThemeWorker extends Worker.Abstract {
99
65
  break;
100
66
  }
101
67
  });
68
+ this.logger.write('Listening for changes...');
102
69
  // quick racing conditions hack
103
70
  setInterval(async () => {
104
71
  const task = this.queue.shift();
@@ -110,32 +77,16 @@ class ThemeWorker extends Worker.Abstract {
110
77
  }
111
78
  enqueue(op, type, name) {
112
79
  this.queue.push(async () => {
113
- try {
114
- const path = Path.join(this.theme.root, type, name);
115
- const payload = {
116
- file_name: name,
117
- file_type: type,
118
- file_operation: op,
119
- };
120
- if (op === 'save') {
121
- payload.file_content = await Form.file(path);
122
- }
123
- await Http.post(`${Env.apiHostname()}/themes/${this.theme.theme_id}/update`, {
124
- body: Form.convert(payload),
125
- });
126
- this.logger.write(`[${op === 'save' ? 'updated' : 'deleted'}] - ${Path.join(type, name)}`);
127
- debounce(() => {
128
- this.io.emit('theme:update');
129
- this.previewLogger.write('reloading preview...');
130
- }, 100)();
131
- }
132
- catch (err) {
133
- if (err instanceof Error) {
134
- this.logger.write(`[error] - ${Path.join(type, name)}\n${err.message}`);
135
- }
136
- }
80
+ await this.execute(op, type, name, true);
81
+ debounce(() => {
82
+ this.io.emit('theme:update');
83
+ this.previewLogger.write('reloading preview...');
84
+ }, 100)();
137
85
  });
138
86
  }
87
+ async execute(op, type, name, log = false) {
88
+ return execute(this.theme, op, type, name, this.logger);
89
+ }
139
90
  }
140
91
 
141
92
  export { ThemeWorker as default };
@@ -1 +1,3 @@
1
+ import type { Metadata } from './types';
1
2
  export declare const THEME_CONFIG_FILENAME = "youcan.theme.json";
3
+ export declare const THEME_FILE_TYPES: Array<keyof Metadata>;
package/dist/constants.js CHANGED
@@ -1,3 +1,12 @@
1
- const THEME_CONFIG_FILENAME = 'youcan.theme.json';
1
+ const THEME_CONFIG_FILENAME = 'youcan.theme.json';
2
+ const THEME_FILE_TYPES = [
3
+ 'layouts',
4
+ 'sections',
5
+ 'locales',
6
+ 'assets',
7
+ 'snippets',
8
+ 'config',
9
+ 'templates',
10
+ ];
2
11
 
3
- export { THEME_CONFIG_FILENAME };
12
+ export { THEME_CONFIG_FILENAME, THEME_FILE_TYPES };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@youcan/theme",
3
3
  "type": "module",
4
- "version": "2.0.5",
4
+ "version": "2.1.0",
5
5
  "description": "OCLIF plugin for building themes",
6
6
  "author": "YouCan <contact@youcan.shop> (https://youcan.shop)",
7
7
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "@oclif/core": "^2.15.0",
19
19
  "debounce": "^2.0.0",
20
20
  "socket.io": "^4.7.2",
21
- "@youcan/cli-kit": "2.0.5"
21
+ "@youcan/cli-kit": "2.1.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@oclif/plugin-legacy": "^1.3.0",