diff --git a/.config/Scripts/Install.sh b/.config/Scripts/Install.sh new file mode 100644 index 0000000..5e9b02c --- /dev/null +++ b/.config/Scripts/Install.sh @@ -0,0 +1,13 @@ +#!usr/bin/env bash + +clear + +deno install \ + --force \ + --global \ + --name slipper \ + --allow-write \ + --allow-read \ + --lock=deno.lock \ + --import-map=Source/Imports.json \ + Source/cli.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d7d1453 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,33 @@ + +################################################################################ +# # +# Editorconfig # +# # +# This file defines basic file properties that # +# editors / IDEs use when you modify documents. # +# # +# Check https://EditorConfig.org for details. # +# # +################################################################################ + + +root = true + + +[*] + +end_of_line = lf + + +[*.{editorconfig,yaml,yml,jsonc,json,html,css,tsx,ts,js,sh,md}] + +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 + + +[*.md] + +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..82a5e36 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ + +name : Publish + + +on : + workflow_dispatch : + push : + branches : [ Stable ] + + +jobs : + publish : + + runs-on : ubuntu-latest + + permissions : + + id-token : write + contents : read + + steps : + + - name : Checkout Repository + uses : actions/checkout@v4 + + - name : Publish To JSR + run : npx jsr publish --config jsr.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..521d689 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +# Symlink to test project + +Test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d11acb2 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ + +# Slipper + +Development tool for @Shopify packing slips. + + + +```sh +deno install \ + --force \ + --global \ + --name slipper \ + --allow-write \ + --allow-read \ + --lock=https://jsr.io/@doom/slipper/1.1.0/deno.lock \ + --import-map=https://jsr.io/@doom/slipper/1.1.0/Imports.json \ + https://jsr.io/@doom/slipper@1.1.0 +``` diff --git a/Source/Arguments/Errors.ts b/Source/Arguments/Errors.ts new file mode 100644 index 0000000..9fd9f0a --- /dev/null +++ b/Source/Arguments/Errors.ts @@ -0,0 +1,15 @@ + +import { ZodError } from 'Zod' +import { cyan , red } from 'Colors' + + +export function fail ( zod : ZodError ){ + + for ( const error of zod.errors ){ + + const { message , path } = error; + + if( message === 'Required' ) + console.error(red(`Argument '${ cyan('' + path) }' is missing`)); + } +} diff --git a/Source/Arguments/Schema.ts b/Source/Arguments/Schema.ts new file mode 100644 index 0000000..0824c23 --- /dev/null +++ b/Source/Arguments/Schema.ts @@ -0,0 +1,12 @@ + +import { z } from 'Zod' + + +export const Schema = z.object({ + config : z.string() , + watch : z.boolean().optional() +}) + + +export type Type = + z.infer diff --git a/Source/Arguments/mod.ts b/Source/Arguments/mod.ts new file mode 100644 index 0000000..63d1f4e --- /dev/null +++ b/Source/Arguments/mod.ts @@ -0,0 +1,21 @@ + +import { Schema } from './Schema.ts' +import { parse } from 'Flags' +import { fail } from './Errors.ts' + + +const { args } = Deno; + + +const parameters = + parse(args); + +const data = Schema + .safeParse(parameters); + +if( ! data.success ){ + fail(data.error); + Deno.exit(1); +} + +export default data.data diff --git a/Source/Config/Errors.ts b/Source/Config/Errors.ts new file mode 100644 index 0000000..ab1b677 --- /dev/null +++ b/Source/Config/Errors.ts @@ -0,0 +1,7 @@ + +import { ZodError } from 'Zod' + + +export function fail ( zod : ZodError ){ + throw zod +} diff --git a/Source/Config/Schema.ts b/Source/Config/Schema.ts new file mode 100644 index 0000000..6663433 --- /dev/null +++ b/Source/Config/Schema.ts @@ -0,0 +1,28 @@ + +import { z } from 'Zod' + + +export const Schema = z.object({ + + Input : z.object({ + Template : z.string() , + Snippets : z + .string() + .optional(), + Styles : z + .string() + .optional() + }), + + Render : z.object({ + Template : z.string() + }), + + Output : z.object({ + Template : z.string() + }) +}) + + +export type Type = + z.infer diff --git a/Source/Config/mod.ts b/Source/Config/mod.ts new file mode 100644 index 0000000..17270ff --- /dev/null +++ b/Source/Config/mod.ts @@ -0,0 +1,30 @@ + +import { Schema } from './Schema.ts' +import { parse } from 'Toml' +import { fail } from './Errors.ts' + +import Arguments from '../Arguments/mod.ts' + + +const { config } = Arguments; + + +const toml = await Deno + .readTextFile(config); + + +const data = parse(toml); + + +const schema = Schema + .safeParse(data); + + +if( ! schema.success ){ + fail(schema.error); + Deno.exit(1); +} + + +export default schema.data + diff --git a/Source/Imports.json b/Source/Imports.json new file mode 100644 index 0000000..c13a07e --- /dev/null +++ b/Source/Imports.json @@ -0,0 +1,14 @@ +{ + "imports" : { + + "Liquid" : "npm:liquidjs@10.11.1" , + "Zod" : "npm:zod@3.23.4" , + + "FileSystem" : "https://deno.land/std@0.224.0/fs/mod.ts" , + "Colors" : "https://deno.land/std@0.224.0/fmt/colors.ts" , + "Flags" : "https://deno.land/std@0.224.0/flags/mod.ts" , + "Toml" : "https://deno.land/std@0.224.0/toml/mod.ts" , + "Path" : "https://deno.land/std@0.224.0/path/mod.ts" , + "Async" : "https://deno.land/std@0.224.0/async/mod.ts" + } +} diff --git a/Source/mod.ts b/Source/mod.ts new file mode 100644 index 0000000..5451db2 --- /dev/null +++ b/Source/mod.ts @@ -0,0 +1,355 @@ + +const { clear , log } = console; + +clear(); + +log(`Starting Slipper`); + + +import Arguments from './Arguments/mod.ts' +import Config from './Config/mod.ts' + +import { basename , dirname , join } from 'Path' +import { debounce } from 'Async' +import { Liquid } from 'Liquid' +import { walk } from 'FileSystem' + + +log('Config:',Config) + + + +const engine = new Liquid({ + extname : '.liquid' , + root : [ ] +}); + + +engine.registerFilter('image_url',image_url); +engine.registerFilter('image_tag',image_tag); + + +function image_url ( url : string, ... args : any [] ){ + return 'Product.jpeg' +} + +function image_tag ( url : string ){ + return `Health potion` +} + + +const context = { + + shop : { + domain : 'Shop.com' , + email : 'Contact@Shop.com' , + name : 'Shopname' + }, + + shop_address : { + + address1 : '107 Enterprise Dr' , + address2 : null , + company : 'Potions Ink' , + city : 'Woodbine' , + province_code : 'IA' , + province : 'Pomorskie' , + zip : '8527' , + country_code : 'IT' , + country : 'Italy' , + summary : '107 Enterprise Dr, Pomorskie, Italy' + }, + + order : { + order_number : 123 , + created_at : '' , + name : '#123' , + note : null + }, + + customer : { + first_name : 'Owen' , + last_name : 'Carter' , + email : 'cornelius.potionmaker@gmail.com' , + name : 'Owen Carter' + }, + + shipping_address : { + address1 : '96 Park Drive' , + address2 : 'Apt 1217' , + first_name : 'Owen' , + last_name : 'Carter' , + name : 'Owen Jay Carter' , + phone : '220-360-8280' , + company : 'McDrive Corp' , + city : 'Henderson' , + province_code : 'KY' , + province : 'Kentucky' , + zip : '42420' , + country_code : 'US' , + country : 'United States' , + summary : '96 Park Drive, Apt 1217, Kentucky, United States' + }, + + billing_address : { + address1 : '150 Elgin Street' , + address2 : '8th floor' , + first_name : null , + last_name : null , + name : '' , + phone : null , + company : `Polina's Potions, LLC` , + city : 'Ottawa' , + province_code : 'ON' , + province : 'Ontario' , + zip : 'K2P 1L4' , + country_code : 'CA' , + country : 'Canada' , + summary : '150 Elgin Street, 8th floor, Ottawa, Ontario, Canada' + }, + + includes_all_line_items_in_order : false , + + line_items_in_shipment : [{ + + image : 'products/Product.png' , + title : 'A Big Book' , + variant_title : 'In Red' , + vendor : 'BigBooks' , + sku : 'F2132123' , + quantity : 2 , + shipping_quantity : 2 , + properties : { + custom : 'test' + } + }] +} + + +const folder_snippets = ( Config.Input.Snippets ) + ? join(dirname(Arguments.config),Config.Input.Snippets) : null + +const folder_styles = ( Config.Input.Styles ) + ? join(dirname(Arguments.config),Config.Input.Styles) : null + +const template_input = + join(dirname(Arguments.config),Config.Input.Template); + +const template_render = + join(dirname(Arguments.config),Config.Render.Template); + +const template_output = + join(dirname(Arguments.config),Config.Output.Template); + + +async function build (){ + + const template = await Deno + .readTextFile(template_input); + + let liquid = template; + + + + const snippets = new Map; + + if( folder_snippets ){ + + const options = { + includeFiles : true , + includeDirs : false , + exts : [ 'liquid' ] + } + + const entries = walk(folder_snippets,options); + + for await ( const entry of entries ) + snippets.set(basename(entry.path),await Deno.readTextFile(entry.path)); + } + + + + console.log('Snippets',snippets); + + + liquid = insertSnippet(liquid) + + + function insertSnippet ( + parent : string + ){ + return parent.replaceAll(//g,( _ : any , path : string ) => { + + const file = `${ path }.liquid`; + + if( ! snippets.has(file) ){ + console.warn(`Couldn't find snippet '${ path }'`); + return '' + } + + + let content = snippets.get(file) ?? '' + + content = insertSnippet(content) + + return content + .split('\n') + .map(( line ) => ` ${ line }` ) + .join('\n') + }) + } + + + liquid = liquid.replaceAll(/\n( *\n){2,}/g,'\n\n') + + + if( folder_styles ){ + + const options = { + includeFiles : true , + includeDirs : false , + exts : [ 'css' , 'liquid' ] + } + + const entries = walk(folder_styles,options); + + for await ( const entry of entries ) + liquid += await Deno.readTextFile(entry.path); + + } + + + await Deno.writeTextFile(template_output,liquid); + + + let html = await engine + .parseAndRender(liquid,undefined,{ + globals : context + }); + + html += ` + + + + + ` + + + const preview = ` + + + + + + + + + + + + ` + + + await Deno.writeTextFile(template_render,preview); +} + + +build() + + + +if( Arguments.watch ){ + + const watched = [ template_input ] + + if( folder_snippets ) + watched.push(folder_snippets) + + if( folder_styles ) + watched.push(folder_styles) + + const watcher = Deno.watchFs(watched,{ + recursive : true + }) + + + await Array.fromAsync( + watcher , + debounce(() => { + + console.clear() + console.debug('Rebuilding') + + build() + + },200) + ) +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..4887ef3 --- /dev/null +++ b/deno.json @@ -0,0 +1,7 @@ +{ + "importMap" : "Source/Imports.json" , + + "tasks" : { + "install" : "bash .config/Scripts/Install.sh" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..3d74533 --- /dev/null +++ b/deno.lock @@ -0,0 +1,26 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "npm:liquidjs@10.11.1": "npm:liquidjs@10.11.1", + "npm:zod@3.23.4": "npm:zod@3.23.4" + }, + "npm": { + "commander@10.0.1": { + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dependencies": {} + }, + "liquidjs@10.11.1": { + "integrity": "sha512-ORlMi7Okt311anHyvuYTRijrdX9He4lbeGEfugSLeHuSYREnFM2M0ppG1/zQHR/k5KSvMP/d4DfUDIFdS8MtqQ==", + "dependencies": { + "commander": "commander@10.0.1" + } + }, + "zod@3.23.4": { + "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", + "dependencies": {} + } + } + }, + "remote": {} +} diff --git a/jsr.json b/jsr.json new file mode 100644 index 0000000..cd3f36c --- /dev/null +++ b/jsr.json @@ -0,0 +1,22 @@ +{ + "$schema" : "https://jsr.io/schema/config-file.v1.json" , + + "name" : "@doom/slipper" , + "version" : "1.1.0" , + + "publish" : { + + "include" : [ + "./Source/Imports.json" , + "./Source/**/*.ts" , + "./README.md" , + "./deno.lock" , + "./jsr.json" , + "./LICENSE" + ], + + "exclude" : [ + "./**/*.test.ts" + ] + } +}