Sharp Scripts

The web .NET Core tool also serve as a #Script runner. The Vue and React "lite" project templates take advantage of this in their Pre-compiled minified production _bundle.ss script which is run with web run {script}:

{{* run in host project directory with `web run wwwroot/_bundle.ss` *}}

{{ false | assignTo: debug }}
{{ (debug ? '' : '.min') | assignTo: min }}
{{ [`/css/bundle${min}.css`,`/js/lib.bundle${min}.js`,`/js/bundle${min}.js`] | map => fileDelete(it) | end }}

{{* Copy same bundle definitions from _layout.html as-is *}}

{{ ['/assets/css/'] | bundleCss({ minify:!debug, cache:!debug, disk:!debug, out:`/css/bundle${min}.css` }) }}

{{ [
    `/lib/vue/dist/vue${min}.js`,
    `/lib/vue-router/dist/vue-router${min}.js`,
    '/lib/vue-class-component/vue-class-component.js',
    '/lib/vue-property-decorator/vue-property-decorator.umd.js',
    '/lib/@servicestack/client/servicestack-client.umd.js',
] | bundleJs({ minify:!debug, cache:!debug, disk:!debug, out:`/js/lib.bundle${min}.js` }) }}

{{ [
    'content:/src/components/',
    'content:/src/shared/',
    'content:/src/',
] | bundleJs({ minify:!debug, cache:!debug, disk:!debug, out:`/js/bundle${min}.js` }) }}

Which can be run with the web tool:

$ dotnet tool install --global web 

$ web run wwwroot/_bundle.ss

Which will create the production bundles, minify all already non-minified bundles and write them to disk with the paths written visible in the #Script *_bundle.ss* output:

<link rel="stylesheet" href="/css/bundle.min.css">
    
<script src="/js/lib.bundle.min.js"></script>

<script src="/js/bundle.min.js"></script>

Sharp Scripts are run in the same context and have access to the same functionality and features as a Sharp App including extensibility va custom plugins. They can run stand-alone independent of an app.settings config file, instead the app settings configuration can be added in its page arguments to enable or configure any features.

Lets go through a couple of different possibilities we can do with scripts:

Adhoc reports

Scripts can use the built-in Database Scripts to be able to run queries against any sqlite, sqlserver, mysql and postgres database and quickly view data snapshots using the built-in HTML Scripts, e.g:

<!--
db sqlite
db.connection ~/../apps/northwind.sqlite
-->

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<style>body {padding:1em} caption{caption-side:top;}</style>
<h1 class="py-2">Order Report #{{id}}</h1>

{{ `SELECT o.Id, OrderDate, CustomerId, Freight, e.Id as EmployeeId, s.CompanyName as ShipVia, 
           ShipAddress, ShipCity, ShipPostalCode, ShipCountry
      FROM "Order" o
           INNER JOIN
           Employee e ON o.EmployeeId = e.Id
           INNER JOIN
           Shipper s ON o.ShipVia = s.Id
     WHERE o.Id = @id` 
  | dbSingle({ id }) | assignTo: order }}

{{#with order}}
  {{ "table table-striped" | assignTo: className }}
  <style>table {border: 5px solid transparent} th {white-space: nowrap}</style>
  
  <div style="display:flex">
      {{ order | htmlDump({ caption: 'Order Details', className }) }}
      {{ `SELECT * FROM Customer WHERE Id = @CustomerId` 
         | dbSingle({ CustomerId }) | htmlDump({ caption: `Customer Details`, className }) }}
      {{ `SELECT Id, LastName, FirstName, Title, City, Country, Extension FROM Employee WHERE Id=@EmployeeId` 
         | dbSingle({ EmployeeId }) | htmlDump({ caption: `Employee Details`, className }) }}
  </div>

  {{ `SELECT p.ProductName, ${sqlCurrency("od.UnitPrice")} UnitPrice, Quantity, Discount
        FROM OrderDetail od
             INNER JOIN
             Product p ON od.ProductId = p.Id
       WHERE OrderId = @id`
      | dbSelect({ id }) 
      | htmlDump({ caption: "Line Items", className }) }}
{{else}}
  {{ `There is no Order with id: ${id}` }}
{{/with}}

Specifying Script Arguments

The above script generates a static HTML page can be invoked with any number of named arguments after the script name, in this case it generates a report for Northwind Order #10643, saves it to 10643.html and opens it in the OS's default browser:

$ web run script.html -id 10643 > 10643.html && start 10643.html

Which looks like:

textDump

Generating static .html pages can quickly produce reports that looks good enough to share with others, but if you just want to see a snapshot info at a glance or be able to share in text-based mediums like email or chat channels you can replace htmlDump with textDump where it will instead output GitHub flavored Markdown tables, e.g:

<!--
db sqlite
db.connection ~/../apps/northwind.sqlite
-->

{{ `SELECT o.Id, OrderDate, CustomerId, Freight, e.Id as EmployeeId, s.CompanyName as ShipVia, 
           ShipAddress, ShipCity, ShipPostalCode, ShipCountry
      FROM "Order" o
           INNER JOIN
           Employee e ON o.EmployeeId = e.Id
           INNER JOIN
           Shipper s ON o.ShipVia = s.Id
     WHERE o.Id = @id` 
  | dbSingle({ id }) | assignTo: order }}

{{#with order}}

{{ order | textDump({ caption: 'Order Details' }) }}

{{ `SELECT p.ProductName, ${sqlCurrency("od.UnitPrice")} UnitPrice, Quantity, Discount
      FROM OrderDetail od
           INNER JOIN
           Product p ON od.ProductId = p.Id
     WHERE OrderId = @id`
    | dbSelect({ id }) 
    | textDump({ caption: "Line Items" }) 
}}
{{ `SELECT ${sqlCurrency("(od.UnitPrice * Quantity)")} AS OrderTotals 
      FROM OrderDetail od
           INNER JOIN
           Product p ON od.ProductId = p.Id
     WHERE OrderId = @id 
     ORDER BY 1 DESC`
    | dbSelect({ id }) 
    | textDump({ rowNumbers: false }) }}
{{else}}
  {{ `There is no Order with id: ${id}` }}
{{/with}}

As the output is human-readable we can view directly it without a browser:

$ web run script.ss -id 10643 

Which will output:

| Order Details                    ||
|------------------|----------------|
| Id               | 10643          |
| Order Date       | 1997-08-25     |
| Customer Id      | ALFKI          |
| Freight          | 29.46          |
| Employee Id      | 6              |
| Ship Via         | Speedy Express |
| Ship Address     | Obere Str. 57  |
| Ship City        | Berlin         |
| Ship Postal Code | 12209          |
| Ship Country     | Germany        |


Line Items

| # | Product Name      | Unit Price | Quantity | Discount |
|---|-------------------|------------|----------|----------|
| 1 | Rössle Sauerkraut | $45.60     | 15       | 0.25     |
| 2 | Chartreuse verte  | $18.00     | 21       | 0.25     |
| 3 | Spegesild         | $12.00     | 2        | 0.25     |


| Order Totals |
|--------------|
| $684.00      |
| $378.00      |
| $24.00       |

And because they're GitHub Flavored Markdown Tables they can be embedded directly in Markdown docs (like this) where it's renders as:

Order Details
Id 10643
Order Date 1997-08-25
Customer Id ALFKI
Freight 29.46
Employee Id 6
Ship Via Speedy Express
Ship Address Obere Str. 57
Ship City Berlin
Ship Postal Code 12209
Ship Country Germany

Line Items

# Product Name Unit Price Quantity Discount
1 Rössle Sauerkraut $45.60 15 0.25
2 Chartreuse verte $18.00 21 0.25
3 Spegesild $12.00 2 0.25
Order Totals
$684.00
$378.00
$24.00

AWS Dashboards

The comprehensive built-in scripts coupled with ServiceStack's agnostic providers like the Virtual File System makes it easy to quickly query infrastructure resources like all Tables and Row counts in managed AWS RDS Instances or Search for static Asset resources in S3 Buckets.

<!--
db postgres
db.connection $AWS_RDS_POSTGRES
files s3
files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}
-->

{{ dbTableNamesWithRowCounts | textDump({ caption: 'Tables' }) }}

{{ `SELECT "Id", "CustomerId", "EmployeeId", "OrderDate" from "Order" ORDER BY "Id" DESC ${sqlLimit(5)}`
   | dbSelect | textDump({ caption: 'Last 5 Orders', headerStyle:'None' }) }}

{{ contentAllRootDirectories | map => `${it.Name}/`
   | union(map(contentAllRootFiles, x => x.Name))
   | textDump({ caption: 'Root Files and Folders' }) }}

{{ find ?? '*.html' | assignTo: find }}
{{ find | contentFilesFind | map => it.VirtualPath | take(15) 
   | textDump({ caption: `Files matching: ${find}` }) }}

You can use $NAME to move confidential information out of public scripts where it will be replaced with Environment Variables. Then run the script as normal and optionally override the find pattern for files you want to search for:

$ web run script-aws.ss -find *.png

Where it displays a dashboard of activity from your AWS resources: containing all Tables with their Row Counts, adhoc queries like your last 5 Orders, The Root files and Folders available in your S3 Bucket and any matching resources from your find search pattern:

| Tables                   ||
|--------------------|------|
| Order Detail       | 2155 |
| Order              | 830  |
| Customer           | 91   |
| Product            | 77   |
| Territory          | 53   |
| Region             | 0    |
| Shipper            | 0    |
| Supplier           | 0    |
| Category           | 0    |
| Employee           | 0    |
| Employee Territory | 0    |


Last 5 Orders

| # | Id    | CustomerId | EmployeeId | OrderDate  |
|---|-------|------------|------------|------------|
| 1 | 11077 | RATTC      | 1          | 1998-05-06 |
| 2 | 11076 | BONAP      | 4          | 1998-05-06 |
| 3 | 11075 | RICSU      | 8          | 1998-05-06 |
| 4 | 11074 | SIMOB      | 7          | 1998-05-06 |
| 5 | 11073 | PERIC      | 2          | 1998-05-05 |


| Root Files and Folders |
|------------------------|
| api/                   |
| northwind/             |
| rockstars/             |
| index.html             |
| web.aws.settings       |
| web.postgres.settings  |
| web.sqlite.settings    |
| web.sqlserver.settings |


| Files matching: *.png                   |
|-----------------------------------------|
| assets/img/logo-32.png                  |
| rockstars/img/green_dust_scratch.png    |
| rockstars/img/rip_jobs.png              |
| rockstars/img/tileable_wood_texture.png |

Azure Dashboards

The nice thing about #Script late-binding and cloud agnostic providers is that with just different configuration we can use the exact same script to query an Azure managed SQL Server Database and Azure Blob File Storage:

<!--
db sqlserver
db.connection $AZURE_SQL_CONNECTION_STRING
files azure
files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}
-->

{{ dbTableNamesWithRowCounts | textDump({ caption: 'Tables' }) }}

{{ `SELECT "Id", "CustomerId", "EmployeeId", "OrderDate" from "Order" ORDER BY "Id" DESC ${sqlLimit(5)}`
   | dbSelect | textDump({ caption: 'Last 5 Orders', headerStyle:'None' }) }}

{{ contentAllRootDirectories | map => `${it.Name}/` 
   | union(map(contentAllRootFiles, x => x.Name))
   | textDump({ caption: 'Root Files and Folders' }) }}

{{ find ?? '*.html' | assignTo: find }}
{{ find | contentFilesFind | map => it.VirtualPath | take(5) 
   | textDump({ caption: `Files matching: ${find}` }) }}

Live #Script with 'web watch'

What's even nicer than the fast feedback of running adhoc scripts? Is the instant feedback you get from being able to "watch" the same script!

To watch a script just replace run with watch:

$ web watch script-aws.ss -find *.png

The ability to run stand-alone adhoc scripts in an extensible dynamic scripting language feels like you're using a "developer enhanced" SQL Studio, where you can combine queries from multiple data sources, manipulate them with LINQ and quickly pipe results to dump utils to combine them in the same output for instant visualization.

#Script scripts can also be easily shared, maintained in gists and run on all different Win/OSX/Linux OS's that .NET Core runs on.

Live Transformations

Another area where "watched" scripts can shine is as a "companion scratch pad" assistant during development that you can quickly switch to and instantly test out live code fragments, calculations and transformations, e.g. This ends up being a great way to test out markdown syntax and Nuglify's advanced compression using our new minifyjs and minifycss Script Blocks:

<!--
debug false
-->

Markdown:
{{#markdown}}
## Title

> quote

Paragraph with [a link](https://example.org).
{{/markdown}}

JS:
{{#minifyjs}}
function add(left, right) {
    return left + right;
}
add(1, 2);
{{/minifyjs}}


CSS:
{{#minifycss}}
body {
    background-color: #ffffff;
}
{{/minifycss}}

Then run with:

$ web watch livepad.ss

Which starts a live watched session that re-renders itself on save, initially with:

Markdown:
<h2 id="title">Title</h2>
<blockquote>
<p>quote</p>
</blockquote>
<p>Paragraph with <a href="https://example.org">a link</a>.</p>

JS:
function add(n,t){return n+t}add(1,2)

CSS:
body{background-color:#fff}

Live Session

made with by ServiceStack