@youcan/theme 2.1.4 → 2.3.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,5 +1,5 @@
1
- import { ThemeCommand } from '@/util/theme-command';
2
- export default class Delete extends ThemeCommand {
3
- static description: string;
4
- run(): Promise<void>;
5
- }
1
+ import { ThemeCommand } from '@/util/theme-command';
2
+ export default class Delete extends ThemeCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -1,33 +1,33 @@
1
1
  import { Session, Http, Env, Tasks } from '@youcan/cli-kit';
2
2
  import { ThemeCommand } from '../../../util/theme-command.js';
3
3
 
4
- class Delete extends ThemeCommand {
5
- static description = 'Select remote development themes to delete';
6
- async run() {
7
- await Session.authenticate(this);
8
- const { dev: themes } = await Http.get(`${Env.apiHostname()}/themes`);
9
- if (!themes.length) {
10
- return this.output.info('You have no remote dev themes');
11
- }
12
- const choices = themes.map(t => ({ title: `${t.name} (${t.id})`, value: t.id }));
13
- const { identifiers } = await this.prompt({
14
- choices,
15
- type: 'multiselect',
16
- name: 'identifiers',
17
- message: 'Select themes to delete',
18
- });
19
- const tasks = identifiers.map((id) => {
20
- const theme = themes.find(t => t.id === id);
21
- return {
22
- title: `Deleting '${theme.name} (${theme.id})'...`,
23
- async task() {
24
- await Http.post(`${Env.apiHostname()}/themes/${theme.id}/delete`);
25
- },
26
- };
27
- });
28
- await Tasks.run({}, tasks);
29
- this.output.info(`Done! ${identifiers.length} themes deleted.`);
30
- }
4
+ class Delete extends ThemeCommand {
5
+ static description = 'Select remote development themes to delete';
6
+ async run() {
7
+ await Session.authenticate(this);
8
+ const { dev: themes } = await Http.get(`${Env.apiHostname()}/themes`);
9
+ if (!themes.length) {
10
+ return this.output.info('You have no remote dev themes');
11
+ }
12
+ const choices = themes.map(t => ({ title: `${t.name} (${t.id})`, value: t.id }));
13
+ const { identifiers } = await this.prompt({
14
+ choices,
15
+ type: 'multiselect',
16
+ name: 'identifiers',
17
+ message: 'Select themes to delete',
18
+ });
19
+ const tasks = identifiers.map((id) => {
20
+ const theme = themes.find(t => t.id === id);
21
+ return {
22
+ title: `Deleting '${theme.name} (${theme.id})'...`,
23
+ async task() {
24
+ await Http.post(`${Env.apiHostname()}/themes/${theme.id}/delete`);
25
+ },
26
+ };
27
+ });
28
+ await Tasks.run({}, tasks);
29
+ this.output.info(`Done! ${identifiers.length} themes deleted.`);
30
+ }
31
31
  }
32
32
 
33
33
  export { Delete as default };
@@ -1,5 +1,5 @@
1
- import { ThemeCommand } from '@/util/theme-command';
2
- export default class Dev extends ThemeCommand {
3
- static description: string;
4
- run(): Promise<void>;
5
- }
1
+ import { ThemeCommand } from '@/util/theme-command';
2
+ export default class Dev extends ThemeCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -1,75 +1,76 @@
1
- import { Session, Tasks, Http, Env, Path, Filesystem, Crypto } from '@youcan/cli-kit';
1
+ import { Session, Tasks, Http, Env, Path, Filesystem, Crypto, UI } 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
5
  import { execute } from '../../services/dev/execute.js';
6
6
  import { THEME_FILE_TYPES } from '../../../constants.js';
7
7
 
8
- class Dev extends ThemeCommand {
9
- static description = 'Start a theme development server and preview your changes';
10
- async run() {
11
- const theme = await load();
12
- await Session.authenticate(this);
13
- const context = await Tasks.run({ cmd: this, workers: [] }, [
14
- {
15
- title: 'Fetching theme metadata...',
16
- async task(ctx) {
17
- const store = await Http.get(`${Env.apiHostname()}/me`);
18
- ctx.store = {
19
- slug: store.slug,
20
- domain: store.domain,
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
- const path = Path.resolve(directory, file);
41
- const isDir = await Filesystem.isDirectory(path);
42
- if (isDir) {
43
- continue;
44
- }
45
- await execute(theme, 'save', type, file);
46
- }
47
- for (const descriptor of descriptors) {
48
- const path = Path.resolve(directory, descriptor.file_name);
49
- if ((await Filesystem.isDirectory(path)) || !(await Filesystem.exists(path))) {
50
- return await execute(theme, '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
- await execute(theme, 'save', type, descriptor.file_name);
56
- }
57
- }
58
- }
59
- },
60
- },
61
- {
62
- title: 'Preparing dev processes...',
63
- async task(ctx) {
64
- ctx.workers = [
65
- new ThemeWorker(ctx.cmd, ctx.store, theme),
66
- ];
67
- await Promise.all(ctx.workers.map(async (w) => await w.boot()));
68
- },
69
- },
70
- ]);
71
- await Promise.all(context.workers.map(async (w) => await w.run()));
72
- }
8
+ class Dev extends ThemeCommand {
9
+ static description = 'Start a theme development server and preview your changes';
10
+ async run() {
11
+ const theme = await load();
12
+ await Session.authenticate(this);
13
+ const context = await Tasks.run({ cmd: this, workers: [] }, [
14
+ {
15
+ title: 'Fetching theme metadata...',
16
+ async task(ctx) {
17
+ const store = await Http.get(`${Env.apiHostname()}/me`);
18
+ ctx.store = {
19
+ slug: store.slug,
20
+ domain: store.domain,
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
+ const path = Path.resolve(directory, file);
41
+ const isDir = await Filesystem.isDirectory(path);
42
+ if (isDir) {
43
+ continue;
44
+ }
45
+ await execute(theme, 'save', type, file);
46
+ }
47
+ for (const descriptor of descriptors) {
48
+ const path = Path.resolve(directory, descriptor.file_name);
49
+ if ((await Filesystem.isDirectory(path)) || !(await Filesystem.exists(path))) {
50
+ return await execute(theme, '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
+ await execute(theme, 'save', type, descriptor.file_name);
56
+ }
57
+ }
58
+ }
59
+ },
60
+ },
61
+ {
62
+ title: 'Preparing dev processes...',
63
+ async task(ctx) {
64
+ ctx.workers = [
65
+ new ThemeWorker(ctx.cmd, ctx.store, theme),
66
+ ];
67
+ await Promise.all(ctx.workers.map(async (w) => await w.boot()));
68
+ },
69
+ },
70
+ ]);
71
+ UI.renderDevOutput({ hotKeys: [], cmd: this });
72
+ await Promise.all(context.workers.map(async (w) => await w.run()));
73
+ }
73
74
  }
74
75
 
75
76
  export { Dev as default };
@@ -1,16 +1,16 @@
1
- import { ThemeCommand } from '@/util/theme-command';
2
- declare class Init extends ThemeCommand {
3
- static description: string;
4
- static args: {
5
- name: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
6
- };
7
- static flags: {
8
- url: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
- inplace: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
- path: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
- 'no-color': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
- verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
- };
14
- run(): Promise<any>;
15
- }
16
- export default Init;
1
+ import { ThemeCommand } from '@/util/theme-command';
2
+ declare class Init extends ThemeCommand {
3
+ static description: string;
4
+ static args: {
5
+ name: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ static flags: {
8
+ url: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
9
+ inplace: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ path: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
11
+ 'no-color': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ run(): Promise<any>;
15
+ }
16
+ export default Init;
@@ -4,120 +4,120 @@ import { ThemeCommand } from '../../../util/theme-command.js';
4
4
  import { THEME_FLAGS } from '../../../flags.js';
5
5
  import { THEME_CONFIG_FILENAME } from '../../../constants.js';
6
6
 
7
- class Init extends ThemeCommand {
8
- static description = 'Clones a theme template git repo';
9
- static args = {
10
- name: Args.string({
11
- name: 'name',
12
- required: false,
13
- description: 'A name for the new theme',
14
- }),
15
- };
16
- static flags = {
17
- ...Cli.commonFlags,
18
- ...THEME_FLAGS,
19
- url: Flags.string({
20
- char: 'u',
21
- env: 'YC_FLAG_CLONE_URL',
22
- description: 'The Git URL to clone from',
23
- default: 'https://github.com/youcan-shop/dusk',
24
- }),
25
- inplace: Flags.boolean({
26
- char: 'i',
27
- default: false,
28
- env: 'YC_FLAG_INPLACE',
29
- description: 'Initialize the current directory as a dev theme instead of cloning',
30
- }),
31
- };
32
- async run() {
33
- const { flags } = await this.parse(Init);
34
- const session = await Session.authenticate(this);
35
- const answers = await (prompt(this));
36
- const dest = Path.join(flags.path, flags.inplace ? '' : answers.theme_name);
37
- await Tasks.run({
38
- payload: answers,
39
- theme_id: null,
40
- cmd: this,
41
- }, [
42
- {
43
- title: `Cloning ${flags.url} into ${dest}...`,
44
- skip: () => flags.inplace,
45
- task: async () => {
46
- await Git.clone({ url: flags.url, destination: dest });
47
- },
48
- },
49
- {
50
- title: 'Initializing development theme...',
51
- task: async (ctx) => {
52
- const path = await Filesystem.archived(dest, answers.theme_name);
53
- const configPath = Path.join(Path.cwd(), THEME_CONFIG_FILENAME);
54
- if (flags.inplace && await Filesystem.exists(configPath)) {
7
+ class Init extends ThemeCommand {
8
+ static description = 'Clones a theme template git repo';
9
+ static args = {
10
+ name: Args.string({
11
+ name: 'name',
12
+ required: false,
13
+ description: 'A name for the new theme',
14
+ }),
15
+ };
16
+ static flags = {
17
+ ...Cli.commonFlags,
18
+ ...THEME_FLAGS,
19
+ url: Flags.string({
20
+ char: 'u',
21
+ env: 'YC_FLAG_CLONE_URL',
22
+ description: 'The Git URL to clone from',
23
+ default: 'https://github.com/youcan-shop/dusk',
24
+ }),
25
+ inplace: Flags.boolean({
26
+ char: 'i',
27
+ default: false,
28
+ env: 'YC_FLAG_INPLACE',
29
+ description: 'Initialize the current directory as a dev theme instead of cloning',
30
+ }),
31
+ };
32
+ async run() {
33
+ const { flags } = await this.parse(Init);
34
+ const session = await Session.authenticate(this);
35
+ const answers = await (prompt(this));
36
+ const dest = Path.join(flags.path, flags.inplace ? '' : answers.theme_name);
37
+ await Tasks.run({
38
+ payload: answers,
39
+ theme_id: null,
40
+ cmd: this,
41
+ }, [
42
+ {
43
+ title: `Cloning ${flags.url} into ${dest}...`,
44
+ skip: () => flags.inplace,
45
+ task: async () => {
46
+ await Git.clone({ url: flags.url, destination: dest });
47
+ },
48
+ },
49
+ {
50
+ title: 'Initializing development theme...',
51
+ task: async (ctx) => {
52
+ const path = await Filesystem.archived(dest, answers.theme_name);
53
+ const configPath = Path.join(Path.cwd(), THEME_CONFIG_FILENAME);
54
+ if (flags.inplace && await Filesystem.exists(configPath)) {
55
55
  throw new Error(`
56
56
  This directory is already linked to a remote theme,
57
57
  please delete youcan.app.json if you wish to create a new one
58
- `);
59
- }
60
- Object.assign(ctx.payload, { archive: await Form.file(path) });
61
- const res = await Http.post(`${Env.apiHostname()}/themes/init`, {
62
- headers: { Authorization: `Bearer ${session.access_token}` },
63
- body: Form.convert(ctx.payload),
64
- });
65
- ctx.theme_id = res.id;
66
- await Filesystem.unlink(path);
67
- },
68
- },
69
- {
70
- title: 'Cleaning up...',
71
- task: async (ctx) => {
72
- await Filesystem.writeJsonFile(Path.join(dest, THEME_CONFIG_FILENAME), { theme_id: ctx.theme_id });
73
- ctx.cmd.output.info(`\nDevelopment theme '${ctx.theme_id}' initiated!`);
74
- },
75
- },
76
- ]);
77
- }
78
- }
79
- async function prompt(command) {
80
- return command.prompt([
81
- {
82
- name: 'theme_name',
83
- type: 'text',
84
- initial: 'Starter',
85
- message: 'Your theme\'s name',
86
- validate: (v) => {
87
- if (!v.length) {
88
- return 'Theme name cannot be empty';
89
- }
90
- if (v.length > 32) {
91
- return 'Theme name cannot exceed 32 characters';
92
- }
93
- return true;
94
- },
95
- },
96
- {
97
- type: 'text',
98
- name: 'theme_author',
99
- message: 'The theme\'s author',
100
- initial: 'YouCan',
101
- },
102
- {
103
- type: 'text',
104
- name: 'theme_version',
105
- message: 'The theme\'s current version',
106
- initial: '1.0.0',
107
- },
108
- {
109
- type: 'text',
110
- name: 'theme_support_url',
111
- message: 'A support URL for this theme.',
112
- initial: 'https://developer.youcan.shop',
113
- },
114
- {
115
- type: 'text',
116
- name: 'theme_documentation_url',
117
- message: 'A documentation URL for this theme.',
118
- initial: 'https://developer.youcan.shop',
119
- },
120
- ]);
58
+ `);
59
+ }
60
+ Object.assign(ctx.payload, { archive: await Form.file(path) });
61
+ const res = await Http.post(`${Env.apiHostname()}/themes/init`, {
62
+ headers: { Authorization: `Bearer ${session.access_token}` },
63
+ body: Form.convert(ctx.payload),
64
+ });
65
+ ctx.theme_id = res.id;
66
+ await Filesystem.unlink(path);
67
+ },
68
+ },
69
+ {
70
+ title: 'Cleaning up...',
71
+ task: async (ctx) => {
72
+ await Filesystem.writeJsonFile(Path.join(dest, THEME_CONFIG_FILENAME), { theme_id: ctx.theme_id });
73
+ ctx.cmd.output.info(`\nDevelopment theme '${ctx.theme_id}' initiated!`);
74
+ },
75
+ },
76
+ ]);
77
+ }
78
+ }
79
+ async function prompt(command) {
80
+ return command.prompt([
81
+ {
82
+ name: 'theme_name',
83
+ type: 'text',
84
+ initial: 'Starter',
85
+ message: 'Your theme\'s name',
86
+ validate: (v) => {
87
+ if (!v.length) {
88
+ return 'Theme name cannot be empty';
89
+ }
90
+ if (v.length > 32) {
91
+ return 'Theme name cannot exceed 32 characters';
92
+ }
93
+ return true;
94
+ },
95
+ },
96
+ {
97
+ type: 'text',
98
+ name: 'theme_author',
99
+ message: 'The theme\'s author',
100
+ initial: 'YouCan',
101
+ },
102
+ {
103
+ type: 'text',
104
+ name: 'theme_version',
105
+ message: 'The theme\'s current version',
106
+ initial: '1.0.0',
107
+ },
108
+ {
109
+ type: 'text',
110
+ name: 'theme_support_url',
111
+ message: 'A support URL for this theme.',
112
+ initial: 'https://developer.youcan.shop',
113
+ },
114
+ {
115
+ type: 'text',
116
+ name: 'theme_documentation_url',
117
+ message: 'A documentation URL for this theme.',
118
+ initial: 'https://developer.youcan.shop',
119
+ },
120
+ ]);
121
121
  }
122
122
 
123
123
  export { Init as default };
@@ -1,5 +1,5 @@
1
- import { ThemeCommand } from '@/util/theme-command';
2
- export default class List extends ThemeCommand {
3
- static description: string;
4
- run(): Promise<void>;
5
- }
1
+ import { ThemeCommand } from '@/util/theme-command';
2
+ export default class List extends ThemeCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -1,32 +1,32 @@
1
1
  import { Session, Http, Env } from '@youcan/cli-kit';
2
2
  import { ThemeCommand } from '../../../util/theme-command.js';
3
3
 
4
- const formatter = Intl.NumberFormat('en', {
5
- notation: 'compact',
6
- style: 'unit',
7
- unit: 'byte',
8
- unitDisplay: 'narrow',
9
- });
10
- class List extends ThemeCommand {
11
- static description = 'List remote development themes';
12
- async run() {
13
- await Session.authenticate(this);
14
- const { dev: themes } = await Http.get(`${Env.apiHostname()}/themes`);
15
- if (!themes.length) {
16
- return this.output.info('You have no remote dev themes');
17
- }
18
- this.output.table(themes.map(t => ({
19
- id: t.id,
20
- name: t.name,
21
- version: t.version,
22
- size: formatter.format(t.size),
23
- })), {
24
- id: { header: 'Identifier' },
25
- name: { header: 'Name' },
26
- version: { header: 'Version' },
27
- size: { header: 'Size' },
28
- });
29
- }
4
+ const formatter = Intl.NumberFormat('en', {
5
+ notation: 'compact',
6
+ style: 'unit',
7
+ unit: 'byte',
8
+ unitDisplay: 'narrow',
9
+ });
10
+ class List extends ThemeCommand {
11
+ static description = 'List remote development themes';
12
+ async run() {
13
+ await Session.authenticate(this);
14
+ const { dev: themes } = await Http.get(`${Env.apiHostname()}/themes`);
15
+ if (!themes.length) {
16
+ return this.output.info('You have no remote dev themes');
17
+ }
18
+ this.output.table(themes.map(t => ({
19
+ id: t.id,
20
+ name: t.name,
21
+ version: t.version,
22
+ size: formatter.format(t.size),
23
+ })), {
24
+ id: { header: 'Identifier' },
25
+ name: { header: 'Name' },
26
+ version: { header: 'Version' },
27
+ size: { header: 'Size' },
28
+ });
29
+ }
30
30
  }
31
31
 
32
32
  export { List as default };
@@ -1,6 +1,6 @@
1
- import { ThemeCommand } from '@/util/theme-command';
2
- export declare function date(): string;
3
- export default class Pack extends ThemeCommand {
4
- static description: string;
5
- run(): Promise<void>;
6
- }
1
+ import { ThemeCommand } from '@/util/theme-command';
2
+ export declare function date(): string;
3
+ export default class Pack extends ThemeCommand {
4
+ static description: string;
5
+ run(): Promise<void>;
6
+ }
@@ -2,45 +2,45 @@ import { Session, Tasks, Path, Filesystem } from '@youcan/cli-kit';
2
2
  import { ThemeCommand } from '../../../util/theme-command.js';
3
3
  import { load } from '../../../util/theme-loader.js';
4
4
 
5
- const formatter = Intl.NumberFormat('en', {
6
- notation: 'compact',
7
- style: 'unit',
8
- unit: 'byte',
9
- unitDisplay: 'narrow',
10
- });
11
- const FILE_TYPES = [
12
- 'layouts',
13
- 'sections',
14
- 'locales',
15
- 'assets',
16
- 'snippets',
17
- 'config',
18
- 'templates',
19
- ];
20
- function date() {
21
- const date = new Date();
22
- const day = date.getDate();
23
- const month = date.getMonth() + 1;
24
- const year = date.getFullYear();
25
- return `${day}-${month}-${year}`;
26
- }
27
- class Pack extends ThemeCommand {
28
- static description = 'Pack your theme into an archive to upload or share';
29
- async run() {
30
- await Session.authenticate(this);
31
- const theme = await load();
32
- const context = await Tasks.run({}, [
33
- {
34
- title: 'Packing your theme...',
35
- async task(ctx) {
36
- const name = `${Path.basename(theme.root)}-${date()}`;
37
- ctx.path = await Filesystem.archived(theme.root, name, `{${FILE_TYPES.join(',')}}/*`);
38
- },
39
- },
40
- ]);
41
- const { size } = await Filesystem.stat(context.path);
42
- this.output.info(`Theme successfully packed into ${Path.basename(context.path)} (${formatter.format(size)})`);
43
- }
5
+ const formatter = Intl.NumberFormat('en', {
6
+ notation: 'compact',
7
+ style: 'unit',
8
+ unit: 'byte',
9
+ unitDisplay: 'narrow',
10
+ });
11
+ const FILE_TYPES = [
12
+ 'layouts',
13
+ 'sections',
14
+ 'locales',
15
+ 'assets',
16
+ 'snippets',
17
+ 'config',
18
+ 'templates',
19
+ ];
20
+ function date() {
21
+ const date = new Date();
22
+ const day = date.getDate();
23
+ const month = date.getMonth() + 1;
24
+ const year = date.getFullYear();
25
+ return `${day}-${month}-${year}`;
26
+ }
27
+ class Pack extends ThemeCommand {
28
+ static description = 'Pack your theme into an archive to upload or share';
29
+ async run() {
30
+ await Session.authenticate(this);
31
+ const theme = await load();
32
+ const context = await Tasks.run({}, [
33
+ {
34
+ title: 'Packing your theme...',
35
+ async task(ctx) {
36
+ const name = `${Path.basename(theme.root)}-${date()}`;
37
+ ctx.path = await Filesystem.archived(theme.root, name, `{${FILE_TYPES.join(',')}}/*`);
38
+ },
39
+ },
40
+ ]);
41
+ const { size } = await Filesystem.stat(context.path);
42
+ this.output.info(`Theme successfully packed into ${Path.basename(context.path)} (${formatter.format(size)})`);
43
+ }
44
44
  }
45
45
 
46
46
  export { date, Pack as default };
@@ -1,4 +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>;
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>;
@@ -1,28 +1,28 @@
1
1
  import { Path, Form, Http, Env } from '@youcan/cli-kit';
2
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
- }
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
26
  }
27
27
 
28
28
  export { execute };
@@ -1,17 +1,17 @@
1
- import { Worker } from '@youcan/cli-kit';
2
- import type { ThemeCommand } from '@/util/theme-command';
3
- import type { 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
- constructor(command: ThemeCommand, store: Store, theme: Theme);
13
- boot(): Promise<void>;
14
- run(): Promise<void>;
15
- private enqueue;
16
- private execute;
17
- }
1
+ import { Worker } from '@youcan/cli-kit';
2
+ import type { ThemeCommand } from '@/util/theme-command';
3
+ import type { 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
+ constructor(command: ThemeCommand, store: Store, theme: Theme);
13
+ boot(): Promise<void>;
14
+ run(): Promise<void>;
15
+ private enqueue;
16
+ private execute;
17
+ }
@@ -1,93 +1,93 @@
1
- import { Worker, Color, Http, System, Path, Filesystem } from '@youcan/cli-kit';
1
+ import { Worker, Http, System, Path, Filesystem } from '@youcan/cli-kit';
2
2
  import { Server } from 'socket.io';
3
3
  import debounce from 'debounce';
4
4
  import { execute } from './execute.js';
5
5
  import { THEME_FILE_TYPES } from '../../../constants.js';
6
6
 
7
- class ThemeWorker extends Worker.Abstract {
8
- command;
9
- store;
10
- theme;
11
- logger;
12
- previewLogger;
13
- queue = [];
14
- io;
15
- constructor(command, store, theme) {
16
- super();
17
- this.command = command;
18
- this.store = store;
19
- this.theme = theme;
20
- this.logger = new Worker.Logger('stdout', 'themes', Color.magenta);
21
- this.previewLogger = new Worker.Logger('stdout', 'preview', Color.dim);
22
- }
23
- async boot() {
24
- try {
25
- this.io = new Server(7565, {
26
- cors: {
27
- origin: `${Http.scheme()}://${this.store.domain}`,
28
- methods: ['GET', 'POST'],
29
- },
30
- });
31
- this.io.on('connection', (socket) => {
32
- this.previewLogger.write(`attached to preview page at ${socket.handshake.address}`);
33
- });
34
- System.open(`${Http.scheme()}://${this.store.domain}/themes/${this.theme.theme_id}/preview`);
35
- }
36
- catch (err) {
37
- this.command.error(err);
38
- }
39
- }
40
- async run() {
41
- const directories = THEME_FILE_TYPES.map(t => Path.resolve(this.theme.root, t));
42
- const watcher = Filesystem.watch(directories, {
43
- awaitWriteFinish: { stabilityThreshold: 50 },
44
- ignoreInitial: true,
45
- persistent: true,
46
- depth: 0,
47
- });
48
- this.command.controller.signal.addEventListener('abort', () => {
49
- watcher.close();
50
- });
51
- watcher.on('all', async (event, path) => {
52
- if (!['add', 'change', 'unlink'].includes(event)) {
53
- return;
54
- }
55
- const [filetype, filename] = [
56
- Path.basename(Path.dirname(path)),
57
- Path.basename(path),
58
- ];
59
- switch (event) {
60
- case 'add':
61
- case 'change':
62
- this.enqueue('save', filetype, filename);
63
- break;
64
- case 'unlink':
65
- this.enqueue('delete', filetype, filename);
66
- break;
67
- }
68
- });
69
- this.logger.write('Listening for changes...');
70
- // quick racing conditions hack
71
- setInterval(async () => {
72
- const task = this.queue.shift();
73
- if (task == null) {
74
- return;
75
- }
76
- await task();
77
- }, 10);
78
- }
79
- enqueue(op, type, name) {
80
- this.queue.push(async () => {
81
- await this.execute(op, type, name, true);
82
- debounce(() => {
83
- this.io.emit('theme:update');
84
- this.previewLogger.write('reloading preview...');
85
- }, 100)();
86
- });
87
- }
88
- async execute(op, type, name, log = false) {
89
- return execute(this.theme, op, type, name, this.logger);
90
- }
7
+ class ThemeWorker extends Worker.Abstract {
8
+ command;
9
+ store;
10
+ theme;
11
+ logger;
12
+ previewLogger;
13
+ queue = [];
14
+ io;
15
+ constructor(command, store, theme) {
16
+ super();
17
+ this.command = command;
18
+ this.store = store;
19
+ this.theme = theme;
20
+ this.logger = new Worker.Logger('themes', 'magenta');
21
+ this.previewLogger = new Worker.Logger('preview', 'dim');
22
+ }
23
+ async boot() {
24
+ try {
25
+ this.io = new Server(7565, {
26
+ cors: {
27
+ origin: `${Http.scheme()}://${this.store.domain}`,
28
+ methods: ['GET', 'POST'],
29
+ },
30
+ });
31
+ this.io.on('connection', (socket) => {
32
+ this.previewLogger.write(`attached to preview page at ${socket.handshake.address}`);
33
+ });
34
+ System.open(`${Http.scheme()}://${this.store.domain}/themes/${this.theme.theme_id}/preview`);
35
+ }
36
+ catch (err) {
37
+ this.command.error(err);
38
+ }
39
+ }
40
+ async run() {
41
+ const directories = THEME_FILE_TYPES.map(t => Path.resolve(this.theme.root, t));
42
+ const watcher = Filesystem.watch(directories, {
43
+ awaitWriteFinish: { stabilityThreshold: 50 },
44
+ ignoreInitial: true,
45
+ persistent: true,
46
+ depth: 0,
47
+ });
48
+ this.command.controller.signal.addEventListener('abort', () => {
49
+ watcher.close();
50
+ });
51
+ watcher.on('all', async (event, path) => {
52
+ if (!['add', 'change', 'unlink'].includes(event)) {
53
+ return;
54
+ }
55
+ const [filetype, filename] = [
56
+ Path.basename(Path.dirname(path)),
57
+ Path.basename(path),
58
+ ];
59
+ switch (event) {
60
+ case 'add':
61
+ case 'change':
62
+ this.enqueue('save', filetype, filename);
63
+ break;
64
+ case 'unlink':
65
+ this.enqueue('delete', filetype, filename);
66
+ break;
67
+ }
68
+ });
69
+ this.logger.write('Listening for changes...');
70
+ // quick racing conditions hack
71
+ setInterval(async () => {
72
+ const task = this.queue.shift();
73
+ if (task == null) {
74
+ return;
75
+ }
76
+ await task();
77
+ }, 10);
78
+ }
79
+ enqueue(op, type, name) {
80
+ this.queue.push(async () => {
81
+ await this.execute(op, type, name, true);
82
+ debounce(() => {
83
+ this.io.emit('theme:update');
84
+ this.previewLogger.write('reloading preview...');
85
+ }, 100)();
86
+ });
87
+ }
88
+ async execute(op, type, name, log = false) {
89
+ return execute(this.theme, op, type, name, this.logger);
90
+ }
91
91
  }
92
92
 
93
93
  export { ThemeWorker as default };
@@ -1,3 +1,3 @@
1
- import type { Metadata } from './types';
2
- export declare const THEME_CONFIG_FILENAME = "youcan.theme.json";
3
- export declare const THEME_FILE_TYPES: Array<keyof Metadata>;
1
+ import type { Metadata } from './types';
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,12 +1,12 @@
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',
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
10
  ];
11
11
 
12
12
  export { THEME_CONFIG_FILENAME, THEME_FILE_TYPES };
package/dist/flags.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const THEME_FLAGS: {
2
- path: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
3
- };
1
+ export declare const THEME_FLAGS: {
2
+ path: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
3
+ };
package/dist/flags.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { Path } from '@youcan/cli-kit';
3
3
 
4
- const THEME_FLAGS = {
5
- path: Flags.string({
6
- env: 'YC_FLAG_PATH',
7
- default: async () => Path.cwd(),
8
- parse: async (input) => Path.resolve(input),
9
- description: 'The path to your theme directory.',
10
- }),
4
+ const THEME_FLAGS = {
5
+ path: Flags.string({
6
+ env: 'YC_FLAG_PATH',
7
+ default: async () => Path.cwd(),
8
+ parse: async (input) => Path.resolve(input),
9
+ description: 'The path to your theme directory.',
10
+ }),
11
11
  };
12
12
 
13
13
  export { THEME_FLAGS };
package/dist/types.d.ts CHANGED
@@ -1,40 +1,42 @@
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
- }
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
+ }
41
+ declare const _default: {};
42
+ export default _default;
package/dist/types.js CHANGED
@@ -1 +1,3 @@
1
+ var types = {};
1
2
 
3
+ export { types as default };
@@ -1,4 +1,4 @@
1
- import { Cli } from '@youcan/cli-kit';
2
- export declare abstract class ThemeCommand extends Cli.Command {
3
- protected configFileName(): string;
4
- }
1
+ import { Cli } from '@youcan/cli-kit';
2
+ export declare abstract class ThemeCommand extends Cli.Command {
3
+ protected configFileName(): string;
4
+ }
@@ -1,10 +1,10 @@
1
1
  import { Cli } from '@youcan/cli-kit';
2
2
  import { THEME_CONFIG_FILENAME } from '../constants.js';
3
3
 
4
- class ThemeCommand extends Cli.Command {
5
- configFileName() {
6
- return THEME_CONFIG_FILENAME;
7
- }
4
+ class ThemeCommand extends Cli.Command {
5
+ configFileName() {
6
+ return THEME_CONFIG_FILENAME;
7
+ }
8
8
  }
9
9
 
10
10
  export { ThemeCommand };
@@ -1,2 +1,2 @@
1
- import type { Theme } from '@/types';
2
- export declare function load(): Promise<Theme>;
1
+ import type { Theme } from '@/types';
2
+ export declare function load(): Promise<Theme>;
@@ -1,16 +1,16 @@
1
1
  import { Path, Filesystem } from '@youcan/cli-kit';
2
2
  import { THEME_CONFIG_FILENAME } from '../constants.js';
3
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
- };
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
14
  }
15
15
 
16
16
  export { load };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@youcan/theme",
3
3
  "type": "module",
4
- "version": "2.1.4",
4
+ "version": "2.3.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.1.4"
21
+ "@youcan/cli-kit": "2.3.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@oclif/plugin-legacy": "^1.3.0",