initial
This commit is contained in:
commit
04877468cf
77 changed files with 29225 additions and 0 deletions
3
.github/copilot-instructions.md
vendored
Normal file
3
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
When talking about Svelte code, please always use Svelte 5 syntax in instructions and code samples unless explicitly asked to use Svelte 4 syntax. Refer to the Svelte llms.txt document at [bin/llms/svelte/llms-small.txt](../bin/llms/svelte/llms-small.txt) in this repo for details on Svelte 5 syntax. Also refer to the SvelteKit llms.txt document at [bin/llms/svelte-kit/llms-small.txt](../bin/llms/svelte-kit/llms-small.txt) in this repo for details on SvelteKit, the framework we use in this repo.
|
||||
|
||||
Many components added to projects come from the `@reuters-graphics/graphics-components` library, which is our team's component library including many Svelte components and other utilities. Refer to the graphics components llms.txt document at [bin/llms/graphics-components/llms.txt](../bin/llms/graphics-components/llms.txt) in this repo for details on working with components from that library and especially for examples of our usual conventions for adding graphics components to a page.
|
||||
50
.github/publish.yaml
vendored
Normal file
50
.github/publish.yaml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
###############################
|
||||
# To publish the project via GitHub Actions:
|
||||
# - move this file into .github/workflows
|
||||
# - uncomment the following lines and configure
|
||||
###############################
|
||||
|
||||
# name: Publish page
|
||||
|
||||
# # These line define what will trigger your project to publish
|
||||
# on:
|
||||
# # ... when you hit the API endpoint for this repo
|
||||
# # Read more: https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event
|
||||
# workflow_dispatch:
|
||||
# # ... whenever you push code to the master branch on GitHub
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# # ... on a cron schedule
|
||||
# schedule:
|
||||
# # Customize to whatever interval you need:
|
||||
# # https://crontab.guru/
|
||||
# - cron: '35 * * * *'
|
||||
#
|
||||
# jobs:
|
||||
# publish-page:
|
||||
# name: Publish page
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# SERVER_WORKFLOW: true
|
||||
# GRAPHICS_SERVER_USERNAME: ${{ secrets.GRAPHICS_SERVER_USERNAME }}
|
||||
# GRAPHICS_SERVER_PASSWORD: ${{ secrets.GRAPHICS_SERVER_PASSWORD }}
|
||||
# GRAPHICS_SERVER_API_KEY: ${{ secrets.GRAPHICS_SERVER_API_KEY }}
|
||||
# SKIP_BUILD_GIT_COMMIT: true
|
||||
# GRAPHICS_SERVER_PUBLISH: true
|
||||
# # This line will notify a Teams channel everytime your project successfully publishes
|
||||
# # GRAPHICS_SERVER_NOTIFY_TEAMS_CHANNEL: # Add a Teams webhook URL here
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: pnpm/action-setup@v4
|
||||
# with:
|
||||
# version: 9
|
||||
# - uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: 20
|
||||
# cache: pnpm
|
||||
# - run: git config user.name github-actions
|
||||
# - run: git config user.email github-actions@github.com
|
||||
# - run: pnpm install
|
||||
# - run: pnpm upload
|
||||
# - run: pnpm publish:publish
|
||||
0
.github/workflows/.keep
vendored
Normal file
0
.github/workflows/.keep
vendored
Normal file
202
.gitignore
vendored
Normal file
202
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,linux
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,linux
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,linux
|
||||
|
||||
.astro/
|
||||
.svelte/
|
||||
.svelte-kit/
|
||||
.graphics-kit/
|
||||
dist/
|
||||
graphics-pack/
|
||||
project-files/docs-ai-ps/
|
||||
src/statics/images/docs-ai-ps
|
||||
.lefthook-local/
|
||||
62
.lefthook/pre-commit/precheck-file-size.js
Normal file
62
.lefthook/pre-commit/precheck-file-size.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import dedent from 'dedent';
|
||||
|
||||
const MAX_SIZE = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
console.log(`💾 Checking staged files for any oversized ...\n`);
|
||||
|
||||
const stagedFiles = execSync('git diff --cached --name-only', {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
|
||||
let hasOversizeFile = false;
|
||||
|
||||
for (const file of stagedFiles) {
|
||||
const filePath = path.resolve(process.cwd(), file);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.size > MAX_SIZE) {
|
||||
console.error(
|
||||
`❌ File too large to commit: ${file} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`
|
||||
);
|
||||
|
||||
execSync(`git reset HEAD "${file}"`);
|
||||
console.log(`↩️ Unstaged from commit`);
|
||||
|
||||
const gitignorePath = path.resolve(process.cwd(), '.gitignore');
|
||||
const gitignoreContents =
|
||||
fs.existsSync(gitignorePath) ?
|
||||
fs.readFileSync(gitignorePath, 'utf-8')
|
||||
: '';
|
||||
|
||||
if (!gitignoreContents.includes(file)) {
|
||||
fs.appendFileSync(
|
||||
gitignorePath,
|
||||
`\n# Auto-ignored oversize file\n${file}\n`
|
||||
);
|
||||
console.log(`✅ Added to .gitignore\n`);
|
||||
}
|
||||
|
||||
hasOversizeFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOversizeFile) {
|
||||
console.log(dedent`
|
||||
💀 GIT COMMIT BLOCKED
|
||||
|
||||
Your project had at least one file larger than 100 MB, which is too large to push to GitHub.
|
||||
|
||||
Any oversize files have been added to your .gitignore and unstaged from the commit.
|
||||
|
||||
Try committing again.
|
||||
`);
|
||||
process.exit(1); // block the commit
|
||||
} else {
|
||||
console.log(`✅ Staged files OK!`);
|
||||
}
|
||||
4
.npmrc
Normal file
4
.npmrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
engine-strict=true
|
||||
side-effects-cache=false
|
||||
auto-install-peers=true
|
||||
node-linker=hoisted
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pnpm-lock.yaml
|
||||
docs/
|
||||
.changeset/
|
||||
10
.prettierrc.js
Normal file
10
.prettierrc.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { svelte as svelteConfig } from '@reuters-graphics/yaks-prettier';
|
||||
|
||||
/**
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
...svelteConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
6
.stackblitzrc
Normal file
6
.stackblitzrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"startCommand": "npm run start",
|
||||
"env": {
|
||||
"STACKBLITZ_ENV": "true"
|
||||
}
|
||||
}
|
||||
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"fivethree.vscode-svelte-snippets",
|
||||
"svelte.svelte-vscode"
|
||||
]
|
||||
}
|
||||
25
.vscode/settings.json
vendored
Normal file
25
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"npm.packageManager": "pnpm",
|
||||
"npm.scriptExplorerAction": "run",
|
||||
"npm.enableRunFromFolder": true,
|
||||
"npm.scriptExplorerExclude": [
|
||||
"build.*",
|
||||
"publish.*",
|
||||
"startup.*",
|
||||
"stories:autolink",
|
||||
"stories:unconfig",
|
||||
"knip",
|
||||
"test",
|
||||
"lint",
|
||||
"format"
|
||||
],
|
||||
"liveshare.launcherClient": "visualStudioCode",
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
}
|
||||
}
|
||||
22
.vscode/svelte.scripts.code-snippets
vendored
Normal file
22
.vscode/svelte.scripts.code-snippets
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"SvelteKit $app/env": {
|
||||
"scope": "javascript",
|
||||
"prefix": "env",
|
||||
"body": [
|
||||
"import { browser, dev, prerendering } from '\\$app/environment';",
|
||||
],
|
||||
"description": "SvelteKit $app/environment stores",
|
||||
},
|
||||
"Static paths": {
|
||||
"scope": "javascript",
|
||||
"prefix": "assets",
|
||||
"body": ["import { assets } from '\\$app/paths';"],
|
||||
"description": "SvelteKit assets path store",
|
||||
},
|
||||
"SvelteKit $app/paths": {
|
||||
"scope": "javascript",
|
||||
"prefix": "paths",
|
||||
"body": ["import { base, assets } from '\\$app/paths';"],
|
||||
"description": "SvelteKit $app/paths stores",
|
||||
},
|
||||
}
|
||||
14
.vscode/svelte.styles.code-snippets
vendored
Normal file
14
.vscode/svelte.styles.code-snippets
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"Svelte SCSS style": {
|
||||
"scope": "svelte",
|
||||
"prefix": "scss",
|
||||
"body": [
|
||||
"<style lang=\"scss\">",
|
||||
" @use '@reuters-graphics/graphics-components/dist/scss/mixins' as mixins;",
|
||||
"",
|
||||
" $1",
|
||||
"</style>",
|
||||
],
|
||||
"description": "Add a Svelte SCSS style tag",
|
||||
},
|
||||
}
|
||||
31
README.md
Normal file
31
README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# hypnagaga
|
||||
|
||||
Graphics created with [bluprint_graphics-kit](https://github.com/reuters-graphics/bluprint_graphics-kit).
|
||||
|
||||
## Developing
|
||||
|
||||
Develop your project.
|
||||
|
||||
```console
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Build and publish preview pages to AWS.
|
||||
|
||||
```console
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
Build and upload your project to Sphinx Graphics Server.
|
||||
|
||||
```console
|
||||
pnpm upload
|
||||
```
|
||||
|
||||
🍻 Publish your project in the Sphinx Graphics Server.
|
||||
|
||||
```console
|
||||
pnpm pub
|
||||
```
|
||||
|
||||
Read more in the [development docs](https://reuters-graphics.github.io/bluprint_graphics-kit/).
|
||||
1
assetinfo.json
Normal file
1
assetinfo.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{ "assetid": ["205245"] }
|
||||
8
bin/connect-docs/EMBED.txt
Normal file
8
bin/connect-docs/EMBED.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<script
|
||||
type="text/javascript"
|
||||
src="https://graphics.thomsonreuters.com/cdn/pym.v1.min.js"
|
||||
></script>
|
||||
<script>
|
||||
var pymParent = new pym.Parent('{{ embedSlug }}', '{{{ embedUrl }}}', {});
|
||||
</script>
|
||||
<div id="{{ embedSlug }}"></div>
|
||||
4
bin/connect-docs/README.txt
Normal file
4
bin/connect-docs/README.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
To embed this graphic in your CMS, use the embed code in the EMBED.txt file in this package.
|
||||
|
||||
You can use the files in app.zip to build the package for hosting on your own servers. See more info at:
|
||||
https://reuters-graphics.github.io/bluprint_graphics-kit/
|
||||
479
bin/llms/graphics-components/llms.txt
Normal file
479
bin/llms/graphics-components/llms.txt
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
## Graphics components
|
||||
|
||||
The `@reuters-graphics/graphics-components` library includes pre-styled components for easily adding graphics or other elements to a page.
|
||||
|
||||
### Adding new components to a page
|
||||
|
||||
Components from the `@reuters-graphics/graphics-components` library are often added to the `#each` loop in `src/lib/App.svelte` that loops over `content.blocks`.
|
||||
|
||||
`content` represents text content pulled from our CMS as JSON that is passed into components via props.
|
||||
|
||||
Each block in `content.blocks` (i.e., "content block") is usually an object with a `type` property and additional properties specific to the block type. For example:
|
||||
|
||||
- **Text Block**:
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"text": "This is a text block."
|
||||
}
|
||||
```
|
||||
- **AI Graphic Block**
|
||||
```json
|
||||
{
|
||||
"type": "ai-graphic",
|
||||
"chart": "AiMap",
|
||||
"width": "normal",
|
||||
"textWidth": "normal",
|
||||
"title": "Optional title of the graphic",
|
||||
"description": "Optional chatter describes more about the graphic.",
|
||||
"notes": "Note: Optional note clarifying something in the data.\r\n\r\nSource: Optional source of the data.",
|
||||
"altText": "Add a description of the graphic for screen readers. This is invisible on the page."
|
||||
}
|
||||
```
|
||||
|
||||
To add a new component to the loop:
|
||||
|
||||
1. Import the component in the script portion of the Svelte component.
|
||||
2. Add a new `else if` condition in the `{#each content.blocks as block}` loop to handle the new block type.
|
||||
3. Pass the required props to the component, ensuring they match the structure of the content block object.
|
||||
|
||||
For example, to add a new FeaturePhoto:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// ...
|
||||
|
||||
import { assets } from '$app/paths';
|
||||
|
||||
import {
|
||||
Article,
|
||||
Analytics,
|
||||
BodyText,
|
||||
EndNotes,
|
||||
SiteHeadline,
|
||||
GraphicBlock,
|
||||
InlineAd,
|
||||
FeautePhoto, // Add the component to others already imported
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
|
||||
// ...
|
||||
</script>
|
||||
|
||||
{#each content.blocks as block}
|
||||
<!-- Text block -->
|
||||
{#if block.type === 'text'}
|
||||
<BodyText text={block.text} />
|
||||
|
||||
<!-- Other block types -->
|
||||
|
||||
<!-- Add new FeaturePhoto block -->
|
||||
{:else if block.type === 'feature-photo'}
|
||||
<FeaturePhoto
|
||||
src="{assets}/{block.src}"
|
||||
alt={block.alt}
|
||||
caption={block.caption}
|
||||
credit={block.credit}
|
||||
/>
|
||||
{:else}
|
||||
<LogBlock message={`Unknown block type: "${block.type}"`} />
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
### Paths to multimedia files
|
||||
|
||||
Notice in the example above, we append the `assets` variable from SvelteKit's `$app/paths` module to the `src` path we got from the content block.
|
||||
|
||||
Always assume that paths to local multimedia files including images and videos specified in content blocks are relative and must be prefixed with the `assets` variable to make them absolute. For example:
|
||||
|
||||
```svelte
|
||||
<FeaturePhoto
|
||||
src="{assets}/{block.src}"
|
||||
alt={block.alt}
|
||||
caption={block.caption}
|
||||
credit={block.credit}
|
||||
/>
|
||||
```
|
||||
|
||||
### Adding AI Graphics
|
||||
|
||||
AI (i.e., Adobe Illustrator) graphics are added to the page by importing a component from the `src/lib/ai2svelte/` directory and then adding that graphic to the `aiCharts` object in `src/lib/App.svelte`. Then the object key for that chart is included in the content block's `chart` property.
|
||||
|
||||
For example, to use an AI graphic from `src/lib/ai2svelte/map.svelte`:
|
||||
|
||||
- The AI graphic component is imported and added to the `aiCharts` object in `src/lib/App.svelte`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
// ...
|
||||
import Map from './ai2svelte/map.svelte';
|
||||
|
||||
// ...
|
||||
|
||||
const aiCharts = {
|
||||
// Other AI graphics ...
|
||||
Map, // Added to the object
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
- Now the content block will specify the key to the chart in `aiCharts` in the `chart` property:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ai-graphic",
|
||||
"chart": "Map",
|
||||
"width": "normal",
|
||||
"textWidth": "normal",
|
||||
"title": "My map",
|
||||
"description": "A map of the area",
|
||||
"notes": "Source: DataSource.org",
|
||||
"altText": "A map of a specific area showing something interesting"
|
||||
}
|
||||
```
|
||||
|
||||
- Now the `{:else if block.type === 'ai-graphic'}` block in `src/lib/App.svelte` uses that key to get the component:
|
||||
|
||||
```svelte
|
||||
{#each content.blocks as block}
|
||||
<!-- Text block -->
|
||||
{#if block.type === 'text'}
|
||||
<BodyText text={block.text} />
|
||||
|
||||
<!-- Ai2svelte graphic block -->
|
||||
{:else if block.type === 'ai-graphic'}
|
||||
{#if !aiCharts[block.chart]}
|
||||
<LogBlock message={`Unable to find "${block.chart}" in aiCharts`} />
|
||||
{:else}
|
||||
{@const AiChart = aiCharts[block.chart]}
|
||||
<GraphicBlock
|
||||
id={block.chart}
|
||||
width={containerWidth(block.width)}
|
||||
title={block.title}
|
||||
description={block.description}
|
||||
notes={block.notes}
|
||||
ariaDescription={block.altText}
|
||||
>
|
||||
<AiChart assetsPath={assets || '/'} />
|
||||
</GraphicBlock>
|
||||
{/if}
|
||||
|
||||
<!-- Other block types -->
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
### Refer to graphics components Storybook
|
||||
|
||||
If you're unsure how to implement a particular graphics component, suggest the user check the Storybook documentation site, which is hosted on GitHub at https://reuters-graphics.github.io/graphics-components/.
|
||||
|
||||
### Writing content blocks in our CMS
|
||||
|
||||
While the text content in the `content` object is formatted as JSON, that data is written into our CMS (which is called "RNGS.io") using ArchieML syntax.
|
||||
|
||||
**When suggesting what content blocks to add for components, please also suggest how to write that content in our CMS (RNGS.io) using ArchieML.**
|
||||
|
||||
#### ArchieML
|
||||
|
||||
ArchieML is a lightweight and intuitive markup language that allows for easy structuring of data within text documents. It is designed to be human-readable, very flexible, and is particularly useful for creating structured data by users who may never have seen ArchieML or any other markup language before.
|
||||
|
||||
##### Basic Syntax
|
||||
|
||||
- **Keys and Values**
|
||||
|
||||
- Definition: Key-value pairs are defined by a line starting with a key followed by a colon. Keys can include any unicode character except whitespace and specific characters used within ArchieML ({ } [ ] : . +).
|
||||
|
||||
- Example:
|
||||
|
||||
```
|
||||
key: This is a value
|
||||
☃: Unicode Snowman for you and you and you!
|
||||
```
|
||||
|
||||
- Parsed JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "This is a value",
|
||||
"☃": "Unicode Snowman for you and you and you!"
|
||||
}
|
||||
```
|
||||
|
||||
- Whitespace around keys and values is ignored. Keys are case-sensitive.
|
||||
|
||||
- **Multi-line Values**: Multi-line values are anchored with `:end`. All whitespace is preserved.
|
||||
|
||||
- Example:
|
||||
|
||||
```
|
||||
key: value
|
||||
More value
|
||||
|
||||
Even more value
|
||||
:end
|
||||
```
|
||||
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"key": "value\n More value\n\nEven more value"
|
||||
}
|
||||
```
|
||||
- Escape Characters: Lines that would be interpreted as keys or commands can be escaped with a backslash `\`.
|
||||
- Example:
|
||||
```
|
||||
key: value
|
||||
\:end
|
||||
:end
|
||||
```
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"key": "value\n:end"
|
||||
}
|
||||
```
|
||||
|
||||
- **Nested Structures**
|
||||
|
||||
- **Dot-Notation**: Use dot-notation for creating nested objects.
|
||||
- Example:
|
||||
```
|
||||
colors.red: #f00
|
||||
colors.green: #0f0
|
||||
colors.blue: #00f
|
||||
```
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"colors": {
|
||||
"red": "#f00",
|
||||
"green": "#0f0",
|
||||
"blue": "#00f"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Object Blocks**: Group keys using object blocks defined by {}. Close an object with {} or by starting a new object.
|
||||
|
||||
- Example:
|
||||
|
||||
```
|
||||
{colors}
|
||||
red: #f00
|
||||
green: #0f0
|
||||
blue: #00f
|
||||
{}
|
||||
|
||||
{numbers}
|
||||
one: 1
|
||||
ten: 10
|
||||
one-hundred: 100
|
||||
{}
|
||||
```
|
||||
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"colors": {
|
||||
"red": "#f00",
|
||||
"green": "#0f0",
|
||||
"blue": "#00f"
|
||||
},
|
||||
"numbers": {
|
||||
"one": "1",
|
||||
"ten": "10",
|
||||
"one-hundred": "100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Arrays**
|
||||
|
||||
- **Arrays of Objects**: Define arrays with brackets [arrayName]. New objects start when the first key is re-encountered.
|
||||
|
||||
- Example:
|
||||
|
||||
```
|
||||
[arrayName]
|
||||
name: Amanda
|
||||
age: 26
|
||||
|
||||
name: Tessa
|
||||
age: 30
|
||||
[]
|
||||
```
|
||||
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"arrayName": [
|
||||
{
|
||||
"name": "Amanda",
|
||||
"age": "26"
|
||||
},
|
||||
{
|
||||
"name": "Tessa",
|
||||
"age": "30"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Arrays of Strings**: Simple arrays use _ for elements. If _ is first, the array ignores key-value pairs.
|
||||
- Example:
|
||||
```
|
||||
[days]
|
||||
* Sunday
|
||||
* Monday
|
||||
* Tuesday
|
||||
* Wednesday
|
||||
* Thursday
|
||||
* Friday
|
||||
* Saturday
|
||||
[]
|
||||
```
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"days": [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
]
|
||||
}
|
||||
```
|
||||
- **Nested Arrays**: Nested arrays use dot notation and are closed with `[]`.
|
||||
|
||||
- Example:
|
||||
|
||||
```
|
||||
[days]
|
||||
name: Monday
|
||||
[.tasks]
|
||||
* Clean dishes
|
||||
* Pick up room
|
||||
[]
|
||||
|
||||
name: Tuesday
|
||||
[.tasks]
|
||||
* Buy milk
|
||||
[]
|
||||
```
|
||||
|
||||
- Parsed JSON:
|
||||
```json
|
||||
{
|
||||
"days": [
|
||||
{
|
||||
"name": "Monday",
|
||||
"tasks": ["Clean dishes", "Pick up room"]
|
||||
},
|
||||
{
|
||||
"name": "Tuesday",
|
||||
"tasks": ["Buy milk"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### ArchieML conventions in our CMS
|
||||
|
||||
Usually, content blocks are written in our CMS as objects inside an ArchieML array called `blocks` and always start with a `type:` key/value pair that defines the type of the content block object.
|
||||
|
||||
For example, a user adding a content block for a FeaturePhoto component, might add the following to the existing `[blocks]` array in our CMS:
|
||||
|
||||
```
|
||||
[blocks]
|
||||
|
||||
type: feature-photo
|
||||
src: images/myPhoto.jpg
|
||||
alt: Alt text for my photo...
|
||||
|
||||
... which extends over multiple lines.
|
||||
:end
|
||||
caption: A photo of something interesting.
|
||||
credit: Jane Doe
|
||||
|
||||
[]
|
||||
```
|
||||
|
||||
### Graphics components style tokens
|
||||
|
||||
As well as Svelte components, the graphics components library includes a tailwind-like style system that can be used to style components and other page elements by adding a class or through SCSS mixins.
|
||||
|
||||
These classes and mixins are defined as individual "tokens" representing the value for an individual style rule, like `font-size` or `color`. Each token sets just one style rule, and multiple tokens are combined together to style an element, like a `<div>`.
|
||||
|
||||
Each set of tokens has several levels that represent the different values a style rule can take in our design system and are grouped in how they're named to make them easier to remember.
|
||||
|
||||
For example, font weight tokens include `font-thin`, `font-light`, and `font-bold` which correspond to the style rules `font-weight: 100;`, `font-weight: 300;`, and `font-weight: 700;`, respectively. And those tokens can be applied via class name or SCSS mixin. For example:
|
||||
|
||||
```svelte
|
||||
<!-- Bold text applied via class -->
|
||||
<p class="font-bold">Here is some bold text with some <span>thin text</span> in it!</p>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
|
||||
|
||||
// Thin text applied via SCSS mixin
|
||||
span {
|
||||
@include mixins.font-thin;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Not all our style tokens have both class names as well as SCSS mixins available to apply them.
|
||||
|
||||
**Please use the tokens defined in the SCSS partials in the [@reuters-graphics/graphics-components/dist/scss/tokens/ directory](./../../../node_modules/@reuters-graphics/graphics-components/dist/scss/tokens/) liberally in instructions and code samples, BUT be sure the token exists before suggesting it. DO NOT MAKE UP TOKENS, CLASS NAMES OR SCSS MIXINS!**
|
||||
|
||||
If you're not sure if there is a token to apply a particular style, you can refer the user to our Storybook documentation site for them at: https://reuters-graphics.github.io/graphics-components/?path=/docs/styles-intro--docs.
|
||||
|
||||
#### Using style tokens
|
||||
|
||||
To use a token to style an element, you can apply it directly to the element through a class name. For example, to apply the `text-primary` token (controlling font colour) you can apply it like this:
|
||||
|
||||
```svelte
|
||||
<p class="text-primary">Lorem ipsum...</p>
|
||||
```
|
||||
|
||||
OR you can apply some tokens via an SCSS mixin. For example:
|
||||
|
||||
```svelte
|
||||
<p>Lorem ipsum...</p>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
|
||||
|
||||
p {
|
||||
@include mixins.text-primary;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Be sure to always include the `@use` line that imports the SCSS mixins from the library in your SCSS/styling suggestions.**
|
||||
|
||||
Please consider the SCSS mixins and classes defined in the [@reuters-graphics/graphics-components/dist/scss/tokens/ directory](./../../../node_modules/@reuters-graphics/graphics-components/dist/scss/tokens/).
|
||||
|
||||
#### Spacing tokens
|
||||
|
||||
We have a special set of tokens to control spacing, i.e., paddings and margins. They operate like tailwind's padding and margin system. For example, `mt-1` represents `margin-top: 0.25rem;` and `px-2` represents `padding-right: 0.5rem; padding-left: 0.5rem;`, etc. These tokens can be applied only through a class.
|
||||
|
||||
These tokens are all defined as a combination of a prefix and a level. The prefix is something like `mb` for bottom margin or `py` for padding top and bottom. The level is a number representing how large the padding are margin should be. The levels go like this: 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, then increasing by 4 each time up to 96. For example, the full token set for top margin would be `mt-0`, `mt-0.5`, `mt-1`, `mt-1.5`, and so on.
|
||||
|
||||
We also have a set of spacing tokens designed to work with _fluid_ typography. These are prefixed beginning with the letter `f`, for example, `fmb-1` represents a _fluid_ margin bottom, `margin-bottom: clamp(0.31rem, 0.31rem + 0vw, 0.31rem);`. These tokens can be applied through a class AND an SCSS mixin. For example:
|
||||
|
||||
```svelte
|
||||
<p class="fmy-3">Some text with margin and padding</p>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
|
||||
|
||||
p {
|
||||
@include mixins.fpx-1;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
You should recommend fluid margin and padding tokens for spacing fluidly-sized typographical elements or elements that are spaced _next to_ fluidly-sized typographical elements. Typographical elements include page headings, paragraphs or elements containing text, generally.
|
||||
893
bin/llms/svelte-kit/llms-small.txt
Normal file
893
bin/llms/svelte-kit/llms-small.txt
Normal file
|
|
@ -0,0 +1,893 @@
|
|||
## Project types
|
||||
|
||||
SvelteKit supports all rendering modes: SPA, SSR, SSG, and you can mix them within one project.
|
||||
|
||||
## Setup
|
||||
|
||||
Scaffold a new SvelteKit project using `npx sv create` then follow the instructions. Do NOT use `npm create svelte` anymore, this command is deprecated.
|
||||
|
||||
A SvelteKit project needs a `package.json` with the following contents at minimum:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do NOT put any of the `devDependencies` listed above into `dependencies`, keep them all in `devDependencies`.
|
||||
|
||||
It also needs a `vite.config.js` with the following at minimum:
|
||||
|
||||
```js
|
||||
import { defineConfig } from 'vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
```
|
||||
|
||||
It also needs a `svelte.config.js` with the following at minimum:
|
||||
|
||||
```js
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
|
||||
export default {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
- **`src/` directory:**
|
||||
- `lib/` for shared code (`$lib`), `lib/server/` for server‑only modules (`$lib/server`), `params/` for matchers, `routes/` for your pages/components, plus `app.html`, `error.html`, `hooks.client.js`, `hooks.server.js`, and `service-worker.js`.
|
||||
- Do **NOT** import server‑only code into client files
|
||||
- **Top‑level assets & configs:**
|
||||
- `static/` for public assets; `tests/` (if using Playwright); config files: `package.json` (with `@sveltejs/kit`, `svelte`, `vite` as devDeps), `svelte.config.js`, `tsconfig.json` (or `jsconfig.json`, extending `.svelte-kit/tsconfig.json`), and `vite.config.js`.
|
||||
- Do **NOT** forget `"type": "module"` in `package.json` if using ESM.
|
||||
- **Build artifacts:**
|
||||
- `.svelte-kit/` is auto‑generated and safe to ignore or delete; it will be recreated on `dev`/`build`.
|
||||
- Do **NOT** commit `.svelte-kit/` to version control.
|
||||
|
||||
## Routing
|
||||
|
||||
- **Filesystem router:** `src/routes` maps directories to URL paths: Everything with a `+page.svelte` file inside it becomes a visitable URL, e.g. `src/routes/hello/+page.svelte` becomes `/hello`. `[param]` folders define dynamic segments. Do NOT use other file system router conventions, e.g. `src/routes/hello.svelte` does NOT become available als URL `/hello`
|
||||
- **Route files:** Prefix with `+`: all run server‑side; only non‑`+server` run client‑side; `+layout`/`+error` apply recursively.
|
||||
- **Best practice:** Do **not** hard‑code routes in code; instead rely on the filesystem convention.
|
||||
|
||||
### +page.svelte
|
||||
|
||||
- Defines UI for a route, SSR on first load and CSR thereafter
|
||||
- Do **not** fetch data inside the component; instead use a `+page.js` or `+page.server.js` `load` function; access its return value through `data` prop via `let { data } = $props()` (typed with `PageProps`).
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
<h1>{data.title}</h1>
|
||||
```
|
||||
|
||||
### +page.js
|
||||
|
||||
- Load data for pages via `export function load({ params })` (typed `PageLoad`), return value is put into `data` prop in component
|
||||
- Can export `prerender`, `ssr`, and `csr` consts here to influence how page is rendered.
|
||||
- Do **not** include private logic (DB or env vars), can **not** export `actions` from here; if needed, use `+page.server.js`.
|
||||
|
||||
```js
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = () => {
|
||||
return {
|
||||
title: 'Hello world!',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### +page.server.js
|
||||
|
||||
- `export async function load(...)` (typed `PageServerLoad`) to access databases or private env; return serializable data.
|
||||
- Can also export `actions` for `<form>` handling on the server.
|
||||
|
||||
### +error.svelte
|
||||
|
||||
- Add `+error.svelte` in a route folder to render an error page, can use `page.status` and `page.error.message` from `$app/state`.
|
||||
- SvelteKit walks up routes to find the closest boundary; falls back to `src/error.html` if none.
|
||||
|
||||
### +layout.svelte
|
||||
|
||||
- Place persistent elements (nav, footer) and include `{@render children()}` to render page content. Example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { LayoutProps } from './$types';
|
||||
let { children, data } = $props();
|
||||
</script>
|
||||
|
||||
<p>Some Content that is shared for all pages below this layout</p>
|
||||
<!-- child layouts/page goes here -->
|
||||
{@render children()}
|
||||
```
|
||||
|
||||
- Create subdirectory `+layout.svelte` to scope UI to nested routes, inheriting parent layouts.
|
||||
- Use layouts to avoid repeating common markup; do **not** duplicate UI in every `+page.svelte`.
|
||||
|
||||
### +layout.js / +layout.server.js
|
||||
|
||||
- In `+layout.js` or `+layout.server.js` export `load()` (typed `LayoutLoad`) to supply `data` to the layout and its children; set `prerender`, `ssr`, `csr`.
|
||||
- Use `+layout.server.js` (typed `LayoutServerLoad`) for server-only things like DB or env access.
|
||||
- Do **not** perform server‑only operations in `+layout.js`; use the server variant.
|
||||
|
||||
```js
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = () => {
|
||||
return {
|
||||
sections: [
|
||||
{ slug: 'profile', title: 'Profile' },
|
||||
{ slug: 'notifications', title: 'Notifications' }
|
||||
]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### +server.js (Endpoints)
|
||||
|
||||
- Export HTTP handlers (`GET`, `POST`, etc.) in `+server.js` under `src/routes`; receive `RequestEvent`, return `Response` or use `json()`, `error()`, `redirect()` (exported from `@sveltejs/kit`).
|
||||
- export `fallback` to catch all other methods.
|
||||
|
||||
```js
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = ({ url }) => {
|
||||
return new Response('hello world');
|
||||
}
|
||||
```
|
||||
|
||||
### $types
|
||||
|
||||
- SvelteKit creates `$types.d.ts` with `PageProps`, `LayoutProps`, `RequestHandler`, `PageLoad`, etc., for type‑safe props and loaders.
|
||||
- Use them inside `+page.svelte`/`+page.server.js`/`+page.js`/`+layout.svelte`/`+layout.server.js`/`+layout.js` by importing from `./$types`
|
||||
|
||||
### Other files
|
||||
|
||||
- Any non‑`+` files in route folders are ignored by the router, use this to your advantage to colocate utilities or components.
|
||||
- For cross‑route imports, place modules under `src/lib` and import via `$lib`.
|
||||
|
||||
## Loading data
|
||||
|
||||
### Page data
|
||||
|
||||
- `+page.js` exports a `load` (`PageLoad`) whose returned object is available in `+page.svelte` via `let { data } = $props()` (e.g. when you do `return { foo }` from `load` it is available within `let { data } = $props()` in `+page.svelte` as `data.foo`)
|
||||
- Universal loads run on SSR and CSR; private or DB‑backed loads belong in `+page.server.js` (`PageServerLoad`) and must return devalue‑serializable data.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
// file: src/routes/foo/+page.js
|
||||
export async function load({ fetch }) {
|
||||
const result = await fetch('/data/from/somewhere').then((r) => r.json());
|
||||
return { result }; // return property "result"
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- file: src/routes/foo/+page.svelte -->
|
||||
<script>
|
||||
// "data" prop contains property "result"
|
||||
let { data } = $props();
|
||||
</script>
|
||||
{data.result}
|
||||
```
|
||||
|
||||
### Layout data
|
||||
|
||||
- `+layout.js` or `+layout.server.js` exports a `load` (`LayoutLoad`/`LayoutServerLoad`)
|
||||
- Layout data flows downward: child layouts and pages see parent data in their `data` prop.
|
||||
- Data loading flow (interaction of load function and props) works the same as for `+page(.server).js/svelte`
|
||||
|
||||
### page.data
|
||||
|
||||
- The `page` object from `$app/state` gives access to all data from `load` functions via `page.data`, usable in any layout or page.
|
||||
- Ideal for things like `<svelte:head><title>{page.data.title}</title></svelte:head>`.
|
||||
- Types come from `App.PageData`
|
||||
- earlier Svelte versions used `$app/stores` for the same concepts, do NOT use `$app/stores` anymore unless prompted to do so
|
||||
|
||||
### Universal vs. server loads
|
||||
|
||||
- Universal (`+*.js`) run on server first, then in browser; server (`+*.server.js`) always run server‑side and can use secrets, cookies, DB, etc.
|
||||
- Both receive `params`, `route`, `url`, `fetch`, `setHeaders`, `parent`, `depends`; server loads additionally get `cookies`, `locals`, `platform`, `request`.
|
||||
- Use server loads for private data or non‑serializable items; universal loads for public APIs or returning complex values (like constructors).
|
||||
|
||||
### Load function arguments
|
||||
|
||||
- `url` is a `URL` object (no `hash` server‑side); `route.id` shows the route pattern; `params` map path segments to values.
|
||||
- Query parameters via `url.searchParams` trigger reruns when they change.
|
||||
- Use these to branch logic and fetch appropriate data in `load`.
|
||||
|
||||
## Making Fetch Requests
|
||||
|
||||
Use the provided `fetch` function for enhanced features:
|
||||
|
||||
```js
|
||||
// src/routes/items/[id]/+page.js
|
||||
export async function load({ fetch, params }) {
|
||||
const res = await fetch(`/api/items/${params.id}`);
|
||||
const item = await res.json();
|
||||
return { item };
|
||||
}
|
||||
```
|
||||
|
||||
## Headers and Cookies
|
||||
|
||||
Set response headers using `setHeaders`:
|
||||
|
||||
```js
|
||||
export async function load({ fetch, setHeaders }) {
|
||||
const response = await fetch(url);
|
||||
|
||||
setHeaders({
|
||||
age: response.headers.get('age'),
|
||||
'cache-control': response.headers.get('cache-control')
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
Access cookies in server load functions using `cookies`:
|
||||
|
||||
```js
|
||||
export async function load({ cookies }) {
|
||||
const sessionid = cookies.get('sessionid');
|
||||
return {
|
||||
user: await db.getUser(sessionid)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Do not set `set-cookie` via `setHeaders`; use `cookies.set()` instead.
|
||||
|
||||
## Using Parent Data
|
||||
|
||||
Access data from parent load functions:
|
||||
|
||||
```js
|
||||
export async function load({ parent }) {
|
||||
const { a } = await parent();
|
||||
return { b: a + 1 };
|
||||
}
|
||||
```
|
||||
|
||||
## Errors and Redirects
|
||||
|
||||
Redirect users using `redirect`:
|
||||
|
||||
```js
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load({ locals }) {
|
||||
if (!locals.user) {
|
||||
redirect(307, '/login');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Throw expected errors using `error`:
|
||||
|
||||
```js
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export function load({ locals }) {
|
||||
if (!locals.user) {
|
||||
error(401, 'not logged in');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Unexpected exceptions trigger `handleError` hook and a 500 response.
|
||||
|
||||
## Streaming with Promises
|
||||
|
||||
Server load functions can stream promises as they resolve:
|
||||
|
||||
```js
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
comments: loadComments(params.slug),
|
||||
post: await loadPost(params.slug)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<h1>{data.post.title}</h1>
|
||||
<div>{@html data.post.content}</div>
|
||||
|
||||
{#await data.comments}
|
||||
Loading comments...
|
||||
{:then comments}
|
||||
{#each comments as comment}
|
||||
<p>{comment.content}</p>
|
||||
{/each}
|
||||
{:catch error}
|
||||
<p>error loading comments: {error.message}</p>
|
||||
{/await}
|
||||
```
|
||||
|
||||
## Rerunning Load Functions
|
||||
|
||||
Load functions rerun when:
|
||||
|
||||
- Referenced params or URL properties change
|
||||
- A parent load function reran and `await parent()` was called
|
||||
- A dependency was invalidated with `invalidate(url)` or `invalidateAll()`
|
||||
|
||||
Manually invalidate load functions:
|
||||
|
||||
```js
|
||||
// In load function
|
||||
export async function load({ fetch, depends }) {
|
||||
depends('app:random');
|
||||
// ...
|
||||
}
|
||||
|
||||
// In component
|
||||
import { invalidate } from '$app/navigation';
|
||||
function rerunLoadFunction() {
|
||||
invalidate('app:random');
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Tracking
|
||||
|
||||
Exclude from dependency tracking with `untrack`:
|
||||
|
||||
```js
|
||||
export async function load({ untrack, url }) {
|
||||
if (untrack(() => url.pathname === '/')) {
|
||||
return { message: 'Welcome!' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implications for authentication
|
||||
|
||||
- Layout loads don’t automatically rerun on CSR; guards in `+layout.server.js` require child pages to await the parent.
|
||||
- To avoid missed auth checks and waterfalls, use hooks like `handle` for global protection or per‑page server loads.
|
||||
|
||||
### Using getRequestEvent
|
||||
|
||||
- `getRequestEvent()` retrieves the current server `RequestEvent`, letting shared functions (e.g. `requireLogin()`) access `locals`, `url`, etc., without parameter passing.
|
||||
|
||||
## Using forms
|
||||
|
||||
### Form actions
|
||||
|
||||
- A `+page.server.js` can export `export const actions: Actions = { default: async (event) => {…} }`; `<form method="POST">` in `+page.svelte` posts to the default action without any JS. `+page.js` or `+layout.js` or `+layout.server.js` can NOT export `actions`
|
||||
- Name multiple actions (`login`, `register`) in `actions`, invoke with `action="?/register"` or `button formaction="?/register"`; do NOT use `default` name in this case.
|
||||
- Each action gets `{ request, cookies, params }`, uses `await request.formData()`, sets cookies or DB state, and returns an object that appears on the page as `form` (typed via `PageProps`).
|
||||
|
||||
Example: Define a default action in `+page.server.js`:
|
||||
|
||||
```js
|
||||
// file: src/routes/login/+page.server.js
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
// TODO log the user in
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Use it with a simple form:
|
||||
|
||||
```svelte
|
||||
<!-- file: src/routes/login/+page.svelte -->
|
||||
<form method="POST">
|
||||
<label>
|
||||
Email
|
||||
<input name="email" type="email">
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input name="password" type="password">
|
||||
</label>
|
||||
<button>Log in</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### Validation errors
|
||||
|
||||
- Return `fail(400, { field, error: true })` from an action to send back status and data; display via `form?.field` and repopulate inputs with `value={form?.field ?? ''}`.
|
||||
- Use `fail` instead of throwing so the nearest `+error.svelte` isn’t invoked and the user can correct their input.
|
||||
- `fail` payload must be JSON‑serializable.
|
||||
|
||||
### Redirects
|
||||
|
||||
- In an action, call `redirect(status, location)` to send a 3xx redirect; this throws and bypasses form re-render.
|
||||
- Client-side, use `goto()` from `$app/navigation` for programmatic redirects.
|
||||
|
||||
### Loading data after actions
|
||||
|
||||
- After an action completes (unless redirected), SvelteKit reruns `load` functions and re‑renders the page, merging the action’s return value into `form`.
|
||||
- The `handle` hook runs once before the action; if you modify cookies in your action, you must also update `event.locals` there to keep `load` in sync.
|
||||
- Do NOT assume `locals` persists automatically; set `event.locals` inside your action when auth state changes.
|
||||
|
||||
### Progressive enhancement
|
||||
|
||||
- Apply `use:enhance` from `$app/forms` to `<form>` to intercept submissions, prevent full reloads, update `form`, `page.form`, `page.status`, reset the form, invalidate all data, handle redirects, render errors, and restore focus. Do NOT use onsubmit event for progressive enhancement
|
||||
- To customize, provide a callback that runs before submit and returns a handler; use `update()` for default logic or `applyAction(result)` to apply form data without full invalidation.
|
||||
- You can also write your own `onsubmit` listener using `fetch`, then `deserialize` the response and `applyAction`/`invalidateAll`; do NOT use `JSON.parse` for action responses.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import type { PageProps } from './$types';
|
||||
import { enhance } from '$app/forms';
|
||||
let { form } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<!-- form content -->
|
||||
</form>
|
||||
```
|
||||
|
||||
## Page options
|
||||
|
||||
#### prerender
|
||||
|
||||
- Set `export const prerender = true|false|'auto'` in page or layout modules; `true` generates static HTML, `false` skips, `'auto'` includes in SSR manifest.
|
||||
- Applies to pages **and** `+server.js` routes (inherit parent flags); dynamic routes need `entries()` or `config.kit.prerender.entries` to tell the crawler which parameter values to use.
|
||||
- Do NOT prerender pages that use form actions or rely on `url.searchParams` server‑side.
|
||||
|
||||
#### entries
|
||||
|
||||
- In a dynamic route’s `+page(.server).js` or `+server.js`, export `export function entries(): Array<Record<string,string>>` (can be async) to list parameter sets for prerendering.
|
||||
- Overrides default crawling to ensure dynamic pages (e.g. `/blog/[slug]`) are generated.
|
||||
- Do NOT forget to pair `entries()` with `export const prerender = true`.
|
||||
|
||||
### ssr
|
||||
|
||||
- `export const ssr = false` disables server-side rendering, sending only an HTML shell and turning the page into a client-only SPA.
|
||||
- Use sparingly (e.g. when using browser‑only globals); do NOT set both `ssr` and `csr` to `false` or nothing will render.
|
||||
|
||||
#### csr
|
||||
|
||||
- `export const csr = false` prevents hydration, omits JS bundle, disables `<script>`s, form enhancements, client routing, and HMR.
|
||||
- Ideal for purely static pages (e.g. marketing or blog posts); do NOT disable CSR on pages requiring interactivity.
|
||||
|
||||
## State management
|
||||
|
||||
- Avoid shared server variables—servers are stateless and shared across users. Authenticate via cookies and persist to a database instead of writing to in‑memory globals.
|
||||
- Keep `load` functions pure: no side‑effects or global store writes. Return data from `load` and pass it via `data` or `page.data`.
|
||||
- For shared client‑only state across components, use Svelte’s context API (`setContext`/`getContext`) or URL parameters for persistent filters; snapshots for ephemeral UI state tied to navigation history.
|
||||
|
||||
## Building your app
|
||||
|
||||
- Build runs in two phases: Vite compiles and prerenders (if enabled), then an adapter tailors output for your deployment target.
|
||||
- Guard any code that should not execute at build time with `import { building } from '$app/environment'; if (!building) { … }`.
|
||||
- Preview your production build locally with `npm run preview` (Node‑only, no adapter hooks).
|
||||
|
||||
## Adapters
|
||||
|
||||
- Adapters transform the built app into deployable assets for various platforms (Cloudflare, Netlify, Node, static, Vercel, plus community adapters).
|
||||
- Configure in `svelte.config.js` under `kit.adapter = adapter(opts)`, importing the adapter module and passing its options.
|
||||
- Some adapters expose a `platform` object (e.g. Cloudflare’s `env`); access it via `event.platform` in hooks and server routes.
|
||||
|
||||
## Single‑page apps
|
||||
|
||||
- Turn your app into a fully CSR SPA by setting `export const ssr = false;` in the root `+layout.js`.
|
||||
- For static hosting, use `@sveltejs/adapter-static` with a `fallback` HTML (e.g. `200.html`) so client routing can handle unknown paths.
|
||||
- You can still prerender select pages by enabling `prerender = true` and `ssr = true` in their individual `+page.js` or `+layout.js` modules.
|
||||
|
||||
## Advanced routing
|
||||
|
||||
- Rest parameters (`[...file]`) capture an unknown number of segments (e.g. `src/routes/hello/[...path]` catches all routes under `/hello`) and expose them as a single string; use a catch‑all route `+error.svelte` to render nested custom 404 pages.
|
||||
- Optional parameters (`[[lang]]`) make a segment optional, e.g. for `[[lang]]/home` both `/home` and `/en/home` map to the same route; cannot follow a rest parameter.
|
||||
- Matchers in `src/params/type.js` let you constrain `[param=type]` (e.g. only “apple” or “orange”), falling back to other routes or a 404 if the test fails.
|
||||
|
||||
### Advanced layouts
|
||||
|
||||
- Group directories `(app)` or `(marketing)` apply a shared layout without affecting URLs.
|
||||
- Break out of the inherited layout chain per page with `+page@segment.svelte` (e.g. `+page@(app).svelte`) or per layout with `+layout@.svelte`.
|
||||
- Use grouping judiciously: overuse can complicate nesting; sometimes simple composition or wrapper components suffice.
|
||||
|
||||
## Hooks
|
||||
|
||||
### Server hooks
|
||||
|
||||
- `handle({ event, resolve })`: runs on every request; mutate `event.locals`, bypass routing, or call `resolve(event, { transformPageChunk, filterSerializedResponseHeaders, preload })` to customize HTML, headers, and asset preloading.
|
||||
- `handleFetch({ event, request, fetch })`: intercepts server‑side `fetch` calls to rewrite URLs, forward cookies on cross‑origin, or route internal requests directly to handlers.
|
||||
- `init()`: runs once at server startup for async setup (e.g. database connections).
|
||||
|
||||
### Shared hooks
|
||||
|
||||
- `handleError({ error, event, status, message })`: catches unexpected runtime errors on server or client; log via Sentry or similar, return a safe object (e.g. `{ message: 'Oops', errorId }`) for `$page.error`.
|
||||
|
||||
### Universal hooks
|
||||
|
||||
- `reroute({ url, fetch? })`: map incoming `url.pathname` to a different route ID (without changing the address bar), optionally async and using `fetch`.
|
||||
- `transport`: define `encode`/`decode` for custom types (e.g. class instances) to serialize them across server/client boundaries in loads and actions.
|
||||
|
||||
## Errors
|
||||
|
||||
- Expected errors thrown with `error(status, message|object)` set the response code, render the nearest `+error.svelte` with `page.error`, and let you pass extra props (e.g. `{ code: 'NOT_FOUND' }`).
|
||||
- Unexpected exceptions invoke the `handleError` hook, are logged internally, and expose a generic `{ message: 'Internal Error' }` to users; customize reporting or user‑safe messages in `handleError`.
|
||||
- Errors in server handlers or `handle` return JSON or your `src/error.html` fallback based on `Accept` headers; errors in `load` render component boundaries as usual. Type‑safe shapes via a global `App.Error` interface.
|
||||
|
||||
## Link options
|
||||
|
||||
The following are HTML attributes you can put on any HTML element.
|
||||
|
||||
- `data-sveltekit-preload-data="hover"|"tap"` preloads `load` on link hover (`touchstart`) or immediate tap; use `"tap"` for fast‑changing data.
|
||||
- `data-sveltekit-preload-code="eager"|"viewport"|"hover"|"tap"` preloads JS/CSS aggressively or on scroll/hover/tap to improve load times.
|
||||
- `data-sveltekit-reload` forces full-page reload; `data-sveltekit-replacestate` uses `replaceState`; `data-sveltekit-keepfocus` retains focus; `data-sveltekit-noscroll` preserves scroll position; disable any by setting the value to `"false"`.
|
||||
|
||||
## Server-only modules
|
||||
|
||||
- `$env/static/private` and `$env/dynamic/private` can only be imported into server‑only files (`hooks.server.js`, `+page.server.js`); prevents leaking secrets to the client.
|
||||
- `$app/server` (e.g. the `read()` API) is likewise restricted to server‑side code.
|
||||
- Make your own modules server‑only by naming them `*.server.js` or placing them in `src/lib/server/`; any public‑facing import chain to these files triggers a build error.
|
||||
|
||||
## Shallow routing
|
||||
|
||||
- Use `pushState(path, state)` or `replaceState('', state)` from `$app/navigation` to create history entries without full navigation; read/write `page.state` from `$app/state`.
|
||||
- Ideal for UI like modals: `if (page.state.showModal) <Modal/>` and dismiss with `history.back()`.
|
||||
- To embed a route’s page component without navigation, preload data with `preloadData(href)` then `pushState`, falling back to `goto`; note SSR and initial load have empty `page.state`, and shallow routing requires JS.
|
||||
|
||||
## Images
|
||||
|
||||
- Vite’s asset handling inlines small files, adds hashes, and lets you `import logo from '...png'` for use in `<img src={logo}>`.
|
||||
- Install `@sveltejs/enhanced-img` and add `enhancedImages()` to your Vite config; use `<enhanced:img src="...jpg" alt="…"/>` to auto‑generate `<picture>` tags with AVIF/WebP, responsive `srcset`/`sizes`, and intrinsic dimensions.
|
||||
- For CMS or dynamic images, leverage a CDN with Svelte libraries like `@unpic/svelte`; always supply high‑resolution originals (2×), specify `sizes` for LCP images, set `fetchpriority="high"`, constrain layout via CSS to avoid CLS, and include meaningful `alt` text.
|
||||
|
||||
## Reference docs
|
||||
|
||||
### Imports from `@sveltejs/kit`
|
||||
|
||||
- **error**: throw an HTTP error and halt request processing
|
||||
|
||||
```js
|
||||
import { error } from '@sveltejs/kit';
|
||||
export function load() {
|
||||
error(404, 'Not found');
|
||||
}
|
||||
```
|
||||
|
||||
- **fail**: return a form action failure without throwing
|
||||
|
||||
```js
|
||||
import { fail } from '@sveltejs/kit';
|
||||
export const actions = {
|
||||
default: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
if (!data.get('name')) return fail(400, { missing: true });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- **isActionFailure**: type‑guard for failures from `fail`
|
||||
|
||||
```js
|
||||
import { isActionFailure } from '@sveltejs/kit';
|
||||
if (isActionFailure(result)) {
|
||||
/* handle invalid form */
|
||||
}
|
||||
```
|
||||
|
||||
- **isHttpError**: type‑guard for errors from `error`
|
||||
|
||||
```js
|
||||
import { isHttpError } from '@sveltejs/kit';
|
||||
try {
|
||||
/* … */
|
||||
} catch (e) {
|
||||
if (isHttpError(e, 404)) console.log('Not found');
|
||||
}
|
||||
```
|
||||
|
||||
- **isRedirect**: type‑guard for redirects from `redirect`
|
||||
|
||||
```js
|
||||
import { redirect, isRedirect } from '@sveltejs/kit';
|
||||
try {
|
||||
redirect(302, '/login');
|
||||
} catch (e) {
|
||||
if (isRedirect(e)) console.log('Redirecting');
|
||||
}
|
||||
```
|
||||
|
||||
- **json**: build a JSON `Response`
|
||||
|
||||
```js
|
||||
import { json } from '@sveltejs/kit';
|
||||
export function GET() {
|
||||
return json({ hello: 'world' });
|
||||
}
|
||||
```
|
||||
|
||||
- **normalizeUrl** _(v2.18+)_: strip internal suffixes/trailing slashes
|
||||
|
||||
```js
|
||||
import { normalizeUrl } from '@sveltejs/kit';
|
||||
const { url, denormalize } = normalizeUrl('/foo/__data.json');
|
||||
url.pathname; // /foo
|
||||
```
|
||||
|
||||
- **redirect**: throw a redirect response
|
||||
|
||||
```js
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
export function load() {
|
||||
redirect(303, '/dashboard');
|
||||
}
|
||||
```
|
||||
|
||||
- **text**: build a plain‑text `Response`
|
||||
|
||||
```js
|
||||
import { text } from '@sveltejs/kit';
|
||||
export function GET() {
|
||||
return text('Hello, text!');
|
||||
}
|
||||
```
|
||||
|
||||
### Imports from `@sveltejs/kit/hooks`
|
||||
|
||||
- **sequence**: compose multiple `handle` hooks into one, merging their options
|
||||
|
||||
```js
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
export const handle = sequence(handleOne, handleTwo);
|
||||
```
|
||||
|
||||
### Imports from `$app/forms`
|
||||
|
||||
- **applyAction**: apply an `ActionResult` to update `page.form` and `page.status`
|
||||
|
||||
```js
|
||||
import { applyAction } from '$app/forms';
|
||||
// inside enhance callback:
|
||||
await applyAction(result);
|
||||
```
|
||||
|
||||
- **deserialize**: parse a serialized form action response back into `ActionResult`
|
||||
|
||||
```js
|
||||
import { deserialize } from '$app/forms';
|
||||
const result = deserialize(await response.text());
|
||||
```
|
||||
|
||||
- **enhance**: progressively enhance a `<form>` for AJAX submissions
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { enhance } from '$app/forms';
|
||||
</script>
|
||||
<form use:enhance on:submit={handle}>
|
||||
```
|
||||
|
||||
### Imports from `$app/navigation`
|
||||
|
||||
- **afterNavigate**: run code after every client‑side navigation. Needs to be called at component initialization
|
||||
|
||||
```js
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
afterNavigate(({ type, to }) => console.log('navigated via', type));
|
||||
```
|
||||
|
||||
- **beforeNavigate**: intercept and optionally cancel upcoming navigations. Needs to be called at component initialization
|
||||
|
||||
```js
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
beforeNavigate(({ cancel }) => {
|
||||
if (!confirm('Leave?')) cancel();
|
||||
});
|
||||
```
|
||||
|
||||
- **disableScrollHandling**: disable automatic scroll resetting after navigation
|
||||
|
||||
```js
|
||||
import { disableScrollHandling } from '$app/navigation';
|
||||
disableScrollHandling();
|
||||
```
|
||||
|
||||
- **goto**: programmatically navigate within the app
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
function navigate() {
|
||||
goto('/dashboard', { replaceState: true });
|
||||
}
|
||||
</script>
|
||||
<button onclick={navigate}>navigate</button>
|
||||
```
|
||||
|
||||
- **invalidate**: re‑run `load` functions that depend on a given URL or custom key
|
||||
|
||||
```js
|
||||
import { invalidate } from '$app/navigation';
|
||||
await invalidate('/api/posts');
|
||||
```
|
||||
|
||||
- **invalidateAll**: re‑run every `load` for the current page
|
||||
|
||||
```js
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
await invalidateAll();
|
||||
```
|
||||
|
||||
- **onNavigate**: hook invoked immediately before client‑side navigations. Needs to be called at component initialization
|
||||
|
||||
```js
|
||||
import { onNavigate } from '$app/navigation';
|
||||
onNavigate(({ to }) => console.log('about to go to', to.url));
|
||||
```
|
||||
|
||||
- **preloadCode**: import route modules ahead of navigation (no data fetch)
|
||||
|
||||
```js
|
||||
import { preloadCode } from '$app/navigation';
|
||||
await preloadCode('/about');
|
||||
```
|
||||
|
||||
- **preloadData**: load both code and data for a route ahead of navigation
|
||||
|
||||
```js
|
||||
import { preloadData } from '$app/navigation';
|
||||
const result = await preloadData('/posts/1');
|
||||
```
|
||||
|
||||
- **pushState**: create a shallow‑routing history entry with custom state
|
||||
|
||||
```js
|
||||
import { pushState } from '$app/navigation';
|
||||
pushState('', { modalOpen: true });
|
||||
```
|
||||
|
||||
- **replaceState**: replace the current history entry with new custom state
|
||||
|
||||
```js
|
||||
import { replaceState } from '$app/navigation';
|
||||
replaceState('', { modalOpen: false });
|
||||
```
|
||||
|
||||
### Imports from `$app/paths`
|
||||
|
||||
- **assets**: the absolute URL prefix for static assets (`config.kit.paths.assets`)
|
||||
|
||||
```js
|
||||
import { assets } from '$app/paths';
|
||||
console.log(`<img src="${assets}/logo.png">`);
|
||||
```
|
||||
|
||||
- **base**: the base path for your app (`config.kit.paths.base`)
|
||||
|
||||
```svelte
|
||||
<a href="{base}/about">About Us</a>
|
||||
```
|
||||
|
||||
- **resolveRoute**: interpolate a route ID with parameters to form a pathname
|
||||
|
||||
```js
|
||||
import { resolveRoute } from '$app/paths';
|
||||
resolveRoute('/blog/[slug]/[...rest]', {
|
||||
slug: 'hello',
|
||||
rest: '2024/updates'
|
||||
});
|
||||
// → "/blog/hello/2024/updates"
|
||||
```
|
||||
|
||||
### Imports from `$app/server`
|
||||
|
||||
- **getRequestEvent** _(v2.20+)_: retrieve the current server `RequestEvent`
|
||||
|
||||
```js
|
||||
import { getRequestEvent } from '$app/server';
|
||||
export function load() {
|
||||
const event = getRequestEvent();
|
||||
console.log(event.url);
|
||||
}
|
||||
```
|
||||
|
||||
- **read** _(v2.4+)_: read a static asset imported by Vite as a `Response`
|
||||
|
||||
```js
|
||||
import { read } from '$app/server';
|
||||
import fileUrl from './data.txt';
|
||||
const res = read(fileUrl);
|
||||
console.log(await res.text());
|
||||
```
|
||||
|
||||
- **navigating**: a read‑only object describing any in‑flight navigation (or `null`)
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { navigating } from '$app/state';
|
||||
console.log(navigating.from, navigating.to);
|
||||
</script>
|
||||
```
|
||||
|
||||
### Imports from `$app/state`
|
||||
|
||||
- **page**: read‑only reactive info about the current page (`url`, `params`, `data`, etc.)
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { page } from '$app/state';
|
||||
const path = $derived(page.url.pathname);
|
||||
</script>
|
||||
{path}
|
||||
```
|
||||
|
||||
- **updated**: reactive flag for new app versions; call `updated.check()` to poll immediately
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { updated } from '$app/state';
|
||||
$effect(() => {
|
||||
if (updated.current) {
|
||||
alert('A new version is available. Refresh?');
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Imports from `$env/dynamic/private`
|
||||
|
||||
- **env (dynamic/private)**: runtime private env vars (`process.env…`), not exposed to client
|
||||
|
||||
```js
|
||||
import { env } from '$env/dynamic/private';
|
||||
console.log(env.SECRET_API_KEY);
|
||||
```
|
||||
|
||||
### Imports from `$env/dynamic/public`
|
||||
|
||||
- **env (dynamic/public)**: runtime public env vars (`PUBLIC_…`), safe for client use
|
||||
|
||||
```js
|
||||
import { env } from '$env/dynamic/public';
|
||||
console.log(env.PUBLIC_BASE_URL);
|
||||
```
|
||||
|
||||
### Imports from `$env/static/private`
|
||||
|
||||
- **$env/static/private**: compile‑time private env vars, dead‑code eliminated
|
||||
|
||||
```js
|
||||
import { DATABASE_URL } from '$env/static/private';
|
||||
console.log(DATABASE_URL);
|
||||
```
|
||||
|
||||
### Imports from `$env/static/public`
|
||||
|
||||
- **$env/static/public**: compile‑time public env vars (`PUBLIC_…`), safe on client
|
||||
|
||||
```js
|
||||
import { PUBLIC_WS_ENDPOINT } from '$env/static/public';
|
||||
console.log(PUBLIC_WS_ENDPOINT);
|
||||
```
|
||||
|
||||
### `$lib` alias
|
||||
|
||||
Alias for `src/lib` folder, e.g.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import Button from '$lib/Button.svelte';
|
||||
</script>
|
||||
<Button>Click me</Button>
|
||||
```
|
||||
|
||||
means that there's a component at `src/lib/Button.svelte`.
|
||||
444
bin/llms/svelte/llms-small.txt
Normal file
444
bin/llms/svelte/llms-small.txt
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
## Svelte
|
||||
|
||||
You **MUST** use the Svelte 5 API unless explicitly tasked to write Svelte 4 syntax. If you don't know about the API yet, below is the most important information about it. Other syntax not explicitly listed like `{#if ...}` blocks stay the same, so you can reuse your Svelte 4 knowledge for these.
|
||||
|
||||
- to mark something a state you use the `$state` rune, e.g. instead of `let count = 0` you do `let count = $state(0)`
|
||||
- to mark something as a derivation you use the `$derived` rune, e.g. instead of `$: double = count * 2` you do `const double = $derived(count * 2)`
|
||||
- to create a side effect you use the `$effect` rune, e.g. instead of `$: console.log(double)`you do`$effect(() => console.log(double))`
|
||||
- to create component props you use the `$props` rune, e.g. instead of `export let foo = true; export let bar;` you do `let { foo = true, bar } = $props();`
|
||||
- when listening to dom events do not use colons as part of the event name anymore, e.g. instead of `<button on:click={...} />` you do `<button onclick={...} />`
|
||||
|
||||
### What are runes?
|
||||
|
||||
- Runes are built-in Svelte keywords (prefixed with `$`) that control the compiler. For example, you write `let message = $state('hello');` in a `.svelte` file.
|
||||
- Do **NOT** treat runes like regular functions or import them; instead, use them as language keywords.
|
||||
_In Svelte 4, this syntax did not exist—you relied on reactive declarations and stores; now runes are an integral part of the language._
|
||||
|
||||
### $state
|
||||
|
||||
- `$state` creates reactive variables that update the UI automatically. For example:
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
</script>
|
||||
<button onclick={() => count++}>Clicked: {count}</button>
|
||||
```
|
||||
- Do **NOT** complicate state management by wrapping it in custom objects; instead, update reactive variables directly.
|
||||
_In Svelte 4, you created state with let, e.g. `let count = 0;`, now use the $state rune, e.g. `let count = $state(0);`._
|
||||
- Arrays and objects become deeply reactive proxies. For example:
|
||||
```js
|
||||
let todos = $state([{ done: false, text: 'add more todos' }]);
|
||||
todos[0].done = !todos[0].done;
|
||||
```
|
||||
- Do **NOT** destructure reactive proxies (e.g., `let { done } = todos[0];`), as this breaks reactivity; instead, access properties directly.
|
||||
- Use `$state` in class fields for reactive properties. For example:
|
||||
```js
|
||||
class Todo {
|
||||
done = $state(false);
|
||||
text = $state('');
|
||||
reset = () => {
|
||||
this.text = '';
|
||||
this.done = false;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### $state.raw
|
||||
|
||||
- `$state.raw` creates shallow state where mutations are not tracked. For example:
|
||||
|
||||
```js
|
||||
let person = $state.raw({ name: 'Heraclitus', age: 49 });
|
||||
// Instead of mutating:
|
||||
// person.age += 1; // NO effect
|
||||
person = { name: 'Heraclitus', age: 50 }; // Correct way to update
|
||||
```
|
||||
|
||||
- Do **NOT** attempt to mutate properties on raw state; instead, reassign the entire object to trigger updates.
|
||||
|
||||
### $state.snapshot
|
||||
|
||||
- `$state.snapshot` produces a plain object copy of reactive state. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let counter = $state({ count: 0 });
|
||||
function logSnapshot() {
|
||||
console.log($state.snapshot(counter));
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- **ONLY** use this if you are told there's a problem with passing reactive proxies to external APIs.
|
||||
|
||||
### Passing state into functions
|
||||
|
||||
- Pass-by-Value Semantics: Use getter functions to ensure functions access the current value of reactive state. For example:
|
||||
```js
|
||||
function add(getA, getB) {
|
||||
return () => getA() + getB();
|
||||
}
|
||||
let a = 1,
|
||||
b = 2;
|
||||
let total = add(
|
||||
() => a,
|
||||
() => b
|
||||
);
|
||||
console.log(total());
|
||||
```
|
||||
- Do **NOT** assume that passing a reactive state variable directly maintains live updates; instead, pass getter functions.
|
||||
_In Svelte 4, you often used stores with subscribe methods; now prefer getter functions with `$state` / `$derived` instead._
|
||||
|
||||
### $derived
|
||||
|
||||
- `$derived` computes reactive values based on dependencies. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let doubled = $derived(count * 2);
|
||||
</script>
|
||||
<button onclick={() => count++}>{doubled}</button>
|
||||
```
|
||||
|
||||
- Do **NOT** introduce side effects in derived expressions; instead, keep them pure.
|
||||
_In Svelte 4 you used `$:` for this, e.g. `$: doubled = count * 2;`, now use the $derived rune instead, e.g `let doubled = $derived(count * 2);`._
|
||||
|
||||
#### $derived.by
|
||||
|
||||
- Use `$derived.by` for multi-line or complex logic. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let numbers = $state([1, 2, 3]);
|
||||
let total = $derived.by(() => {
|
||||
let sum = 0;
|
||||
for (const n of numbers) sum += n;
|
||||
return sum;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
- Do **NOT** force complex logic into a single expression; instead, use `$derived.by` to keep code clear.
|
||||
|
||||
#### Overriding derived values
|
||||
|
||||
- You can reassign a derived value for features like optimistic UI. It will go back to the `$derived` value once an update in its dependencies happen. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let post = $props().post;
|
||||
let likes = $derived(post.likes);
|
||||
async function onclick() {
|
||||
likes += 1;
|
||||
try { await post.like(); } catch { likes -= 1; }
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- Do **NOT** try to override derived state via effects; instead, reassign directly when needed.
|
||||
_In Svelte 4 you could use `$:` for that, e.g. `$: likes = post.likes; likes = 1`, now use the `$derived` instead, e.g. `let likes = $derived(post.likes); likes = 1;`._
|
||||
|
||||
### $effect
|
||||
|
||||
- `$effect` executes functions when reactive state changes. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let size = $state(50);
|
||||
$effect(() => {
|
||||
console.log('Size changed:', size);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
- Do **NOT** use `$effect` for state synchronization; instead, use it only for side effects like logging or DOM manipulation.
|
||||
_In Svelte 4, you used reactive statements (`$:`) for similar tasks, .e.g `$: console.log(size)`; now use the `$effect` rune instead, e.g. `$effect(() => console.log(size))` ._
|
||||
|
||||
#### Understanding lifecycle (for $effect)
|
||||
|
||||
- Effects run after the DOM updates and can return teardown functions. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
$effect(() => {
|
||||
const interval = setInterval(() => { count += 1; }, 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
- **Directive:** Do **NOT** ignore cleanup; instead, always return a teardown function when needed.
|
||||
|
||||
#### $effect.pre
|
||||
|
||||
- `$effect.pre` works like `$effect` with the only difference that it runs before the DOM updates. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let div = $state();
|
||||
$effect.pre(() => {
|
||||
if (div) console.log('Running before DOM update');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
- Do **NOT** use `$effect.pre` for standard post-update tasks; instead, reserve it for pre-DOM manipulation like autoscrolling.
|
||||
|
||||
#### $effect.tracking
|
||||
|
||||
- `$effect.tracking` indicates if code is running inside a reactive context. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
$effect(() => {
|
||||
console.log('Inside effect, tracking:', $effect.tracking());
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
- Do **NOT** misuse tracking information outside its intended debugging context; instead, use it to enhance reactive debugging.
|
||||
_In Svelte 4, no equivalent existed; now this feature offers greater insight into reactivity._
|
||||
|
||||
#### $effect.root
|
||||
|
||||
- `$effect.root` creates a non-tracked scope for nested effects with manual cleanup. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
const cleanup = $effect.root(() => {
|
||||
$effect(() => {
|
||||
console.log('Count is:', count);
|
||||
});
|
||||
return () => console.log('Root effect cleaned up');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
- Do **NOT** expect root effects to auto-cleanup; instead, manage their teardown manually.
|
||||
_In Svelte 4, manual cleanup required explicit lifecycle hooks; now `$effect.root` centralizes this control._
|
||||
|
||||
### $props
|
||||
|
||||
- Use `$props` to access component inputs. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { adjective } = $props();
|
||||
</script>
|
||||
<p>This component is {adjective}</p>
|
||||
```
|
||||
|
||||
- Do **NOT** mutate props directly; instead, use callbacks or bindable props to communicate changes.
|
||||
_In Svelte 4, props were declared with `export let foo`; now you use `$props` rune, e.g. `let { foo } = $props()`._
|
||||
- Declare fallback values via destructuring. For example:
|
||||
|
||||
```js
|
||||
let { adjective = 'happy' } = $props();
|
||||
```
|
||||
|
||||
- Rename props to avoid reserved keywords. For example:
|
||||
|
||||
```js
|
||||
let { super: trouper } = $props();
|
||||
```
|
||||
|
||||
- Use rest syntax to collect all remaining props. For example:
|
||||
|
||||
```js
|
||||
let { a, b, ...others } = $props();
|
||||
```
|
||||
|
||||
#### $props.id()
|
||||
|
||||
- Generate a unique ID for the component instance. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
const uid = $props.id();
|
||||
</script>
|
||||
<label for="{uid}-firstname">First Name:</label>
|
||||
<input id="{uid}-firstname" type="text" />
|
||||
```
|
||||
|
||||
- Do **NOT** manually generate or guess IDs; instead, rely on `$props.id()` for consistency.
|
||||
|
||||
### $bindable
|
||||
|
||||
- Mark props as bindable to allow two-way data flow. For example, in `FancyInput.svelte`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { value = $bindable() } = $props();
|
||||
</script>
|
||||
<input bind:value={value} />
|
||||
```
|
||||
|
||||
- Do **NOT** overuse bindable props; instead, default to one-way data flow unless bi-directionality is truly needed.
|
||||
_In Svelte 4, all props were implicitly bindable; in Svelte 5 `$bindable` makes this explicit._
|
||||
|
||||
### $host
|
||||
|
||||
- Only available inside custom elements. Access the host element for custom event dispatching. For example:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
function dispatch(type) {
|
||||
$host().dispatchEvent(new CustomEvent(type));
|
||||
}
|
||||
</script>
|
||||
<button onclick={() => dispatch('increment')}>Increment</button>
|
||||
```
|
||||
|
||||
- Do **NOT** use this unless you are explicitly tasked to create a custom element using Svelte components
|
||||
|
||||
### {#snippet ...}
|
||||
|
||||
- **Definition & Usage:**
|
||||
Snippets allow you to define reusable chunks of markup with parameters inside your component.
|
||||
_Example:_
|
||||
```svelte
|
||||
{#snippet figure(image)}
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
{/snippet}
|
||||
```
|
||||
- **Parameterization:**
|
||||
Snippets accept multiple parameters with optional defaults and destructuring, but rest parameters are not allowed.
|
||||
_Example with parameters:_
|
||||
```svelte
|
||||
{#snippet name(param1, param2)}
|
||||
<!-- snippet markup here -->
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
### Snippet scope
|
||||
|
||||
- **Lexical Visibility:**
|
||||
Snippets can be declared anywhere and reference variables from their outer lexical scope, including script or block-level declarations.
|
||||
_Example:_
|
||||
```svelte
|
||||
<script>
|
||||
let { message = "it's great to see you!" } = $props();
|
||||
</script>
|
||||
{#snippet hello(name)}
|
||||
<p>hello {name}! {message}!</p>
|
||||
{/snippet}
|
||||
{@render hello('alice')}
|
||||
```
|
||||
- **Scope Limitations:**
|
||||
Snippets are only accessible within their lexical scope; siblings and child blocks share scope, but nested snippets cannot be rendered outside.
|
||||
_Usage caution:_ Do **NOT** attempt to render a snippet outside its declared scope.
|
||||
|
||||
### Passing snippets to components
|
||||
|
||||
- **As Props:**
|
||||
Within a template, snippets are first-class values that can be passed to components as props.
|
||||
_Example:_
|
||||
```svelte
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 }
|
||||
];
|
||||
</script>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
<Table data={fruits} {header} {row} />
|
||||
```
|
||||
- **Slot-like Behavior:**
|
||||
Snippets declared inside component tags become implicit props (akin to slots) for the component.
|
||||
_Svelte 4 used slots for this, e.g. `<Component><p slot="x" let:y>hi {y}</p></Component>`; now use snippets instead, e.g. `<Component>{#snippet x(y)}<p>hi {y}</p>{/snippet}</Component>`._
|
||||
- **Content Fallback:**
|
||||
Content not wrapped in a snippet declaration becomes the `children` snippet, rendering as fallback content.
|
||||
_Example:_
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
<Button>click me</Button>
|
||||
<!-- Button.svelte -->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
### Typing snippets
|
||||
|
||||
- Snippets implement the `Snippet` interface, enabling strict type checking in TypeScript or JSDoc.
|
||||
_Example:_
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
interface Props {
|
||||
data: any[];
|
||||
children: Snippet;
|
||||
row: Snippet<[any]>;
|
||||
}
|
||||
let { data, children, row }: Props = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
### {@render ...}
|
||||
|
||||
- Use the {@render ...} tag to invoke and render a snippet, passing parameters as needed.
|
||||
_Example:_
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
<p>{a} + {b} = {a + b}</p>
|
||||
{/snippet}
|
||||
{@render sum(1, 2)}
|
||||
```
|
||||
- Do **NOT** call snippets without parentheses when parameters are required; instead, always invoke the snippet correctly.
|
||||
_In Svelte 4, you used slots for this, e.g. `<slot name="sum" {a} {b} />`; now use `{@render}` instead, e.g. `{@render sum(a,b)}`._
|
||||
|
||||
### <svelte:boundary>
|
||||
|
||||
- Use error boundary tags to prevent rendering errors in a section from crashing the whole app.
|
||||
_Example:_
|
||||
|
||||
```svelte
|
||||
<svelte:boundary onerror={(error, reset) => console.error(error)}>
|
||||
<FlakyComponent />
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
- **Failed Snippet for Fallback UI:**
|
||||
Providing a `failed` snippet renders fallback content when an error occurs and supplies a `reset` function.
|
||||
_Example:_
|
||||
|
||||
```svelte
|
||||
<svelte:boundary>
|
||||
<FlakyComponent />
|
||||
{#snippet failed(error, reset)}
|
||||
<button onclick={reset}>Oops! Try again</button>
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
```
|
||||
|
||||
### class
|
||||
|
||||
- Svelte 5 allows objects for conditional class assignment using truthy keys. It closely follows the `clsx` syntax
|
||||
_Example:_
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { cool } = $props();
|
||||
</script>
|
||||
<div class={{ cool, lame: !cool }}>Content</div>
|
||||
```
|
||||
14
bin/mods/_utils/locations.ts
Normal file
14
bin/mods/_utils/locations.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import * as find from 'empathic/find';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Get primary locations for the project.
|
||||
*
|
||||
* **Note:** These need to be derived from the CWD to work
|
||||
* with tests in the temporary working directory.
|
||||
*/
|
||||
export const getLocations = () => {
|
||||
const PKG = find.up('package.json', { cwd: process.cwd() })!;
|
||||
const ROOT = path.dirname(PKG);
|
||||
return { PKG, ROOT };
|
||||
};
|
||||
143
bin/mods/_utils/mod/fs.ts
Normal file
143
bin/mods/_utils/mod/fs.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { utils } from '@reuters-graphics/graphics-bin';
|
||||
|
||||
/**
|
||||
* Class for managing file operations such as swapping, copying, moving, and removing files.
|
||||
*/
|
||||
export class FileMover {
|
||||
/**
|
||||
* Ensures the given path is treated as an array of path parts.
|
||||
* Converts a string path to an array containing the single path.
|
||||
* @param pathOrParts - The path as a string or an array of strings.
|
||||
* @returns An array of path parts.
|
||||
*/
|
||||
private ensureArrayPath(pathOrParts: string | string[]): string[] {
|
||||
return Array.isArray(pathOrParts) ? pathOrParts : [pathOrParts];
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins path parts into a full path string.
|
||||
* @param pathOrParts - The path as a string or an array of strings.
|
||||
* @returns The absolute path string.
|
||||
*/
|
||||
private getAbsolutePath(pathOrParts: string | string[]): string {
|
||||
return path.join(...this.ensureArrayPath(pathOrParts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps two files, optionally archiving the original destination file.
|
||||
* If an archive path is provided, the destination file is moved there.
|
||||
* The source file is moved to the destination path.
|
||||
*
|
||||
* @param srcPath - Source file path (string or array of path parts).
|
||||
* @param destPath - Destination file path (string or array of path parts).
|
||||
* @param archivePath - Archive file path (string or array of path parts).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fileMover = new FileMover();
|
||||
* fileMover.swap('src.txt', 'dest.txt', 'archive/dest.txt');
|
||||
* ```
|
||||
*/
|
||||
swap(
|
||||
srcPath: string | string[],
|
||||
destPath: string | string[],
|
||||
archivePath: string | string[]
|
||||
) {
|
||||
const absSrcPath = this.getAbsolutePath(srcPath);
|
||||
const absDestPath = this.getAbsolutePath(destPath);
|
||||
const absArchivePath = this.getAbsolutePath(archivePath);
|
||||
|
||||
if ((absArchivePath === '.' && absSrcPath === '.') || absDestPath === '.') {
|
||||
throw new Error('Invalid swap');
|
||||
}
|
||||
|
||||
if (absArchivePath !== '.') {
|
||||
if (!fs.existsSync(absDestPath))
|
||||
throw new Error(`File not found: ${absDestPath}`);
|
||||
utils.fs.ensureDir(absArchivePath);
|
||||
fs.renameSync(absDestPath, absArchivePath);
|
||||
if (fs.readdirSync(path.dirname(absDestPath)).length === 0)
|
||||
fs.rmSync(path.dirname(absDestPath), { recursive: true });
|
||||
}
|
||||
|
||||
if (absSrcPath !== '.') {
|
||||
if (!fs.existsSync(absSrcPath))
|
||||
throw new Error(`File not found: ${absSrcPath}`);
|
||||
utils.fs.ensureDir(absDestPath);
|
||||
fs.renameSync(absSrcPath, absDestPath);
|
||||
if (fs.readdirSync(path.dirname(absSrcPath)).length === 0)
|
||||
fs.rmSync(path.dirname(absSrcPath), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file from the source path to the destination path.
|
||||
* Ensures the destination directory exists before copying.
|
||||
*
|
||||
* @param srcPath - Source file path (string or array of path parts).
|
||||
* @param destPath - Destination file path (string or array of path parts).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fileMover = new FileMover();
|
||||
* fileMover.copy('src.txt', 'dest.txt');
|
||||
* ```
|
||||
*/
|
||||
copy(srcPath: string | string[], destPath: string | string[]) {
|
||||
const absSrcPath = this.getAbsolutePath(srcPath);
|
||||
const absDestPath = this.getAbsolutePath(destPath);
|
||||
|
||||
if (!fs.existsSync(absSrcPath))
|
||||
throw new Error(`File not found: ${absSrcPath}`);
|
||||
|
||||
utils.fs.ensureDir(absDestPath);
|
||||
fs.copyFileSync(absSrcPath, absDestPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file from the source path to the destination path.
|
||||
* Ensures the destination directory exists before moving.
|
||||
*
|
||||
* @param srcPath - Source file path (string or array of path parts).
|
||||
* @param destPath - Destination file path (string or array of path parts).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fileMover = new FileMover();
|
||||
* fileMover.move('src.txt', 'dest.txt');
|
||||
* ```
|
||||
*/
|
||||
move(srcPath: string | string[], destPath: string | string[]) {
|
||||
const absSrcPath = this.getAbsolutePath(srcPath);
|
||||
const absDestPath = this.getAbsolutePath(destPath);
|
||||
|
||||
if (!fs.existsSync(absSrcPath))
|
||||
throw new Error(`File not found: ${absSrcPath}`);
|
||||
|
||||
utils.fs.ensureDir(absDestPath);
|
||||
fs.renameSync(absSrcPath, absDestPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a file or directory at the specified path.
|
||||
* The removal is recursive and forced, meaning it will delete directories and their contents.
|
||||
*
|
||||
* @param filePath - File or directory path (string or array of path parts).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const fileMover = new FileMover();
|
||||
* fileMover.remove('old-file.txt');
|
||||
* ```
|
||||
*/
|
||||
remove(filePath: string | string[]) {
|
||||
const absFilePath = this.getAbsolutePath(filePath);
|
||||
|
||||
if (!fs.existsSync(absFilePath))
|
||||
throw new Error(`File not found: ${absFilePath}`);
|
||||
|
||||
fs.rmSync(absFilePath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
11
bin/mods/_utils/mod/index.ts
Normal file
11
bin/mods/_utils/mod/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { FileMover } from './fs';
|
||||
import { MagicFile } from './magicFile';
|
||||
import { PackageJsonManager } from './pkg';
|
||||
|
||||
export class Mod {
|
||||
pkg = new PackageJsonManager();
|
||||
fs = new FileMover();
|
||||
magicFile(filePath: string) {
|
||||
return new MagicFile(filePath);
|
||||
}
|
||||
}
|
||||
109
bin/mods/_utils/mod/magicFile.test.ts
Normal file
109
bin/mods/_utils/mod/magicFile.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import mock from 'mock-fs';
|
||||
import dedent from 'dedent';
|
||||
import fs from 'fs';
|
||||
import { MagicFile } from './magicFile';
|
||||
|
||||
describe('MagicFile Tests', () => {
|
||||
beforeEach(() => {
|
||||
mock({
|
||||
'example.ts': dedent`
|
||||
const x = 10;
|
||||
const y = 20;
|
||||
console.log(x + y);
|
||||
const z = x + y;
|
||||
`,
|
||||
'example.js': dedent`
|
||||
let a = 5;
|
||||
let b = 10;
|
||||
console.log(a + b);
|
||||
`,
|
||||
'example.json': dedent`
|
||||
{
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
}
|
||||
`,
|
||||
'example.svelte': dedent`
|
||||
<script>
|
||||
let count = 0;
|
||||
</script>
|
||||
<p>The total is {count}.</p>
|
||||
`,
|
||||
'example.scss': dedent`
|
||||
.class {
|
||||
color: red;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should append content to a specific line in a .ts file', () => {
|
||||
const magicFile = new MagicFile('example.ts');
|
||||
|
||||
magicFile.findOffsetLine(/console.log/).appendToLine(' // Added comment');
|
||||
|
||||
magicFile.findOffsetLine('const z =').prependToLine('// ');
|
||||
|
||||
magicFile.saveFile();
|
||||
|
||||
const updatedContent = fs.readFileSync('example.ts', 'utf-8');
|
||||
expect(updatedContent).toContain('console.log(x + y); // Added comment');
|
||||
expect(updatedContent).toContain('// const z');
|
||||
});
|
||||
|
||||
it('should prepend content to a specific line in a .js file', () => {
|
||||
const magicFile = new MagicFile('example.js');
|
||||
|
||||
magicFile
|
||||
.offsetLine(1)
|
||||
.prependLeft(0, '// Initialize variables\n')
|
||||
.saveFile();
|
||||
|
||||
const updatedContent = fs.readFileSync('example.js', 'utf-8');
|
||||
expect(updatedContent).toContain('// Initialize variables\nlet a = 5;');
|
||||
});
|
||||
|
||||
it('should overwrite content in a .json file', () => {
|
||||
const magicFile = new MagicFile('example.json');
|
||||
const magicString = magicFile.findOffsetLine(/"key2": "value2"/);
|
||||
|
||||
magicString.replaceLine(' "key2": "newValue"');
|
||||
magicString.saveFile();
|
||||
|
||||
const updatedContent = fs.readFileSync('example.json', 'utf-8');
|
||||
expect(updatedContent).toContain(' "key2": "newValue"');
|
||||
});
|
||||
|
||||
it('should append content to a specific line in a .svelte file', () => {
|
||||
const magicFile = new MagicFile('example.svelte');
|
||||
|
||||
magicFile.replaceAll('count', 'number').saveFile();
|
||||
|
||||
const updatedContent = fs.readFileSync('example.svelte', 'utf-8');
|
||||
expect(updatedContent).toContain(
|
||||
dedent`
|
||||
<script>
|
||||
let number = 0;
|
||||
</script>
|
||||
<p>The total is {number}.</p>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should overwrite content in a .scss file', () => {
|
||||
const magicFile = new MagicFile('example.scss');
|
||||
const magicString = magicFile.findOffsetLine('color: red;');
|
||||
|
||||
magicString.overwrite(0, 11, 'color: blue;');
|
||||
magicString.saveFile();
|
||||
|
||||
const updatedContent = fs.readFileSync('example.scss', 'utf-8');
|
||||
expect(updatedContent).toContain('color: blue;');
|
||||
expect(updatedContent).not.toContain('color: red;');
|
||||
});
|
||||
});
|
||||
103
bin/mods/_utils/mod/magicFile.ts
Normal file
103
bin/mods/_utils/mod/magicFile.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import fs from 'fs';
|
||||
import MagicString from 'magic-string';
|
||||
|
||||
export class MagicFile extends MagicString {
|
||||
private filePath: string;
|
||||
|
||||
constructor(filePath: string) {
|
||||
if (!fs.existsSync(filePath)) throw new Error('File not found');
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
super(content);
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
saveFile() {
|
||||
fs.writeFileSync(this.filePath, this.toString(), 'utf-8');
|
||||
}
|
||||
|
||||
private getLineOffset(lineNumber: number) {
|
||||
const lines = this.original.split('\n');
|
||||
if (lineNumber < 0 || lineNumber >= lines.length) {
|
||||
throw new Error('Line number out of range');
|
||||
}
|
||||
|
||||
return lines
|
||||
.slice(0, lineNumber)
|
||||
.reduce((offset, line) => offset + line.length + 1, 0); // +1 for the newline character
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the MagicString offset by the first line that includes the search.
|
||||
*
|
||||
* @param search Search string or RegEx
|
||||
* @param offsetLineNumber Line number after which to start searching (0-based index)
|
||||
* @returns MagicString instance
|
||||
*/
|
||||
findOffsetLine(search: string | RegExp, offsetLineNumber: number = 0) {
|
||||
const lines = this.original.split('\n');
|
||||
if (offsetLineNumber < 0 || offsetLineNumber >= lines.length) {
|
||||
throw new Error('Offset line number out of range');
|
||||
}
|
||||
|
||||
const lineIndex = lines
|
||||
.slice(offsetLineNumber + 1)
|
||||
.findIndex((line) =>
|
||||
typeof search === 'string' ? line.includes(search) : search.test(line)
|
||||
);
|
||||
|
||||
if (lineIndex === -1) {
|
||||
throw new Error('Content not found');
|
||||
}
|
||||
|
||||
const adjustedLineIndex = offsetLineNumber + 1 + lineIndex;
|
||||
|
||||
this.offset = this.getLineOffset(adjustedLineIndex);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the MagicString offset by line index. (Lines start at 1.)
|
||||
*
|
||||
* @param {number} lineNumber
|
||||
* @returns MagicString instance
|
||||
*/
|
||||
offsetLine(lineNumber: number) {
|
||||
const lines = this.original.split('\n');
|
||||
if (lineNumber <= 0 || lineNumber > lines.length) {
|
||||
throw new Error('Line number out of range');
|
||||
}
|
||||
|
||||
this.offset = this.getLineOffset(lineNumber - 1);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private getEndOfLineIndex() {
|
||||
const lineEndIndex = this.original.indexOf('\n', this.offset);
|
||||
const endIndex =
|
||||
lineEndIndex === -1 ? this.original.length : lineEndIndex - this.offset;
|
||||
return endIndex;
|
||||
}
|
||||
|
||||
appendToLine(content: string) {
|
||||
const endOfLineIndex = this.getEndOfLineIndex();
|
||||
return this.appendRight(endOfLineIndex, content);
|
||||
}
|
||||
|
||||
replaceLine(content: string) {
|
||||
const endOfLineIndex = this.getEndOfLineIndex();
|
||||
return this.overwrite(this.offset, endOfLineIndex, content);
|
||||
}
|
||||
|
||||
prependToLine(content: string) {
|
||||
return this.appendLeft(0, content);
|
||||
}
|
||||
|
||||
locations(search: string) {
|
||||
const start = this.original.indexOf(search);
|
||||
if (start === -1) throw new Error('Search string not found');
|
||||
const end = start + search.length;
|
||||
return { start, end };
|
||||
}
|
||||
}
|
||||
102
bin/mods/_utils/mod/pkg.ts
Normal file
102
bin/mods/_utils/mod/pkg.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import fs from 'fs';
|
||||
import * as find from 'empathic/find';
|
||||
|
||||
export class PackageJsonManager {
|
||||
private packageJson: {
|
||||
scripts: Record<string, string>;
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
};
|
||||
private filePath: string;
|
||||
|
||||
constructor() {
|
||||
this.filePath = find.up('package.json', { cwd: process.cwd() })!;
|
||||
this.loadPackageJson();
|
||||
}
|
||||
|
||||
private loadPackageJson(): void {
|
||||
if (fs.existsSync(this.filePath)) {
|
||||
const data = fs.readFileSync(this.filePath, 'utf-8');
|
||||
this.packageJson = JSON.parse(data);
|
||||
} else {
|
||||
throw new Error('package.json file not found.');
|
||||
}
|
||||
}
|
||||
|
||||
private savePackageJson(): void {
|
||||
fs.writeFileSync(
|
||||
this.filePath,
|
||||
JSON.stringify(this.packageJson, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
private sortObjectKeys(obj: Record<string, string>): Record<string, string> {
|
||||
return Object.keys(obj)
|
||||
.sort()
|
||||
.reduce((result: Record<string, string>, key: string) => {
|
||||
result[key] = obj[key];
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
addDependency(name: string, version: string): void {
|
||||
if (!this.packageJson.dependencies) {
|
||||
this.packageJson.dependencies = {};
|
||||
}
|
||||
this.packageJson.dependencies[name] = version;
|
||||
this.packageJson.dependencies = this.sortObjectKeys(
|
||||
this.packageJson.dependencies
|
||||
);
|
||||
this.savePackageJson();
|
||||
}
|
||||
|
||||
removeDependency(name: string): void {
|
||||
if (this.packageJson.dependencies && this.packageJson.dependencies[name]) {
|
||||
delete this.packageJson.dependencies[name];
|
||||
this.packageJson.dependencies = this.sortObjectKeys(
|
||||
this.packageJson.dependencies
|
||||
);
|
||||
this.savePackageJson();
|
||||
}
|
||||
}
|
||||
|
||||
addDevDependency(name: string, version: string): void {
|
||||
if (!this.packageJson.devDependencies) {
|
||||
this.packageJson.devDependencies = {};
|
||||
}
|
||||
this.packageJson.devDependencies[name] = version;
|
||||
this.packageJson.devDependencies = this.sortObjectKeys(
|
||||
this.packageJson.devDependencies
|
||||
);
|
||||
this.savePackageJson();
|
||||
}
|
||||
|
||||
removeDevDependency(name: string): void {
|
||||
if (
|
||||
this.packageJson.devDependencies &&
|
||||
this.packageJson.devDependencies[name]
|
||||
) {
|
||||
delete this.packageJson.devDependencies[name];
|
||||
this.packageJson.devDependencies = this.sortObjectKeys(
|
||||
this.packageJson.devDependencies
|
||||
);
|
||||
this.savePackageJson();
|
||||
}
|
||||
}
|
||||
|
||||
addScript(name: string, command: string): void {
|
||||
if (!this.packageJson.scripts) {
|
||||
this.packageJson.scripts = {};
|
||||
}
|
||||
this.packageJson.scripts[name] = command;
|
||||
this.savePackageJson();
|
||||
}
|
||||
|
||||
removeScript(name: string): void {
|
||||
if (this.packageJson.scripts && this.packageJson.scripts[name]) {
|
||||
delete this.packageJson.scripts[name];
|
||||
this.savePackageJson();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
bin/mods/export-ai-statics/index.ts
Normal file
73
bin/mods/export-ai-statics/index.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { cancel, isCancel, log, select } from '@clack/prompts';
|
||||
import { utils } from '@reuters-graphics/graphics-bin';
|
||||
import { AiExport } from '@reuters-graphics/illustrator-exports';
|
||||
import { getLocations } from '../_utils/locations';
|
||||
import fs from 'fs';
|
||||
import { globSync } from 'glob';
|
||||
import path from 'path';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
|
||||
export const exportAiStatics = async () => {
|
||||
const { ROOT } = getLocations();
|
||||
|
||||
const aiFiles = globSync('*.ai', {
|
||||
cwd: path.join(ROOT, 'project-files'),
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
const aiFile = await select({
|
||||
message: 'Which AI file would you like to export assets for?',
|
||||
options: aiFiles.map((filePath) => ({
|
||||
label: path.basename(filePath, '.ai'),
|
||||
value: filePath,
|
||||
})),
|
||||
});
|
||||
if (isCancel(aiFile)) return cancel();
|
||||
|
||||
const locale = await select({
|
||||
message: "What's the language for this graphic?",
|
||||
initialValue: 'en',
|
||||
options: [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'ar', label: 'Arabic' },
|
||||
{ value: 'fr', label: 'French' },
|
||||
{ value: 'es', label: 'Spanish' },
|
||||
{ value: 'de', label: 'German' },
|
||||
{ value: 'it', label: 'Italian' },
|
||||
{ value: 'ja', label: 'Japanese' },
|
||||
{ value: 'pt', label: 'Portugese' },
|
||||
{ value: 'ru', label: 'Russian' },
|
||||
],
|
||||
});
|
||||
if (isCancel(locale)) return cancel();
|
||||
|
||||
const aiSlug = slugify(path.basename(aiFile, '.ai'));
|
||||
const aiExport = new AiExport(aiFile);
|
||||
const mediaJPG = path.join(
|
||||
ROOT,
|
||||
'media-assets',
|
||||
locale,
|
||||
aiSlug,
|
||||
'graphic.jpg'
|
||||
);
|
||||
const mediaEPS = path.join(
|
||||
ROOT,
|
||||
'media-assets',
|
||||
locale,
|
||||
aiSlug,
|
||||
'graphic.eps'
|
||||
);
|
||||
const staticsJPG = path.join(
|
||||
ROOT,
|
||||
'src/statics/images/embeds/',
|
||||
locale,
|
||||
aiSlug + '.jpg'
|
||||
);
|
||||
aiExport.saveEPS(mediaEPS, '-static');
|
||||
log.step(`Exported EPS: ${path.relative(ROOT, mediaEPS)}`);
|
||||
aiExport.exportJPG(mediaJPG, '-static');
|
||||
log.step(`Exported JPG: ${path.relative(ROOT, mediaJPG)}`);
|
||||
utils.fs.ensureDir(staticsJPG);
|
||||
fs.copyFileSync(mediaJPG, staticsJPG);
|
||||
log.step(`Exported JPG: ${path.relative(ROOT, staticsJPG)}`);
|
||||
};
|
||||
55
bin/mods/index.ts
Normal file
55
bin/mods/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import sade from 'sade';
|
||||
import { changeProjectType } from './project-type';
|
||||
import { intro } from '@reuters-graphics/clack';
|
||||
import { cancel, isCancel, log, outro, select } from '@clack/prompts';
|
||||
import { exportAiStatics } from './export-ai-statics';
|
||||
import { makeAiEmbed } from './make-ai-embed';
|
||||
import { unconfigRngsIo } from './rngs-io';
|
||||
|
||||
const prog = sade('kit-mods');
|
||||
|
||||
prog
|
||||
.command('project-type')
|
||||
.option('-f, --force', 'Force the change', false)
|
||||
.action(async (opts) => {
|
||||
intro('Kit mods');
|
||||
log.step('Change project type');
|
||||
await changeProjectType(!!opts.force);
|
||||
outro('Done.');
|
||||
});
|
||||
|
||||
prog.command('mods').action(async () => {
|
||||
intro('Kit mods');
|
||||
const mod = await select({
|
||||
message: 'Which mod do you want?',
|
||||
options: [
|
||||
{
|
||||
value: 'export-ai-statics',
|
||||
label: 'Export AI statics',
|
||||
hint: 'export JPG and EPS files',
|
||||
},
|
||||
{
|
||||
value: 'make-ai-embed',
|
||||
label: 'Make an embed page',
|
||||
hint: 'for ai2svelte graphics',
|
||||
},
|
||||
{
|
||||
value: 'project-type',
|
||||
label: 'Change my project type',
|
||||
hint: 'to embeds-only or pages+',
|
||||
},
|
||||
],
|
||||
initialValue: 'export-ai-statics',
|
||||
});
|
||||
if (isCancel(mod)) return cancel();
|
||||
if (mod === 'export-ai-statics') await exportAiStatics();
|
||||
if (mod === 'make-ai-embed') await makeAiEmbed();
|
||||
if (mod === 'project-type') await changeProjectType();
|
||||
outro('Done.');
|
||||
});
|
||||
|
||||
prog.command('unconfig-rngs-io').action(() => {
|
||||
unconfigRngsIo();
|
||||
});
|
||||
|
||||
prog.parse(process.argv);
|
||||
61
bin/mods/make-ai-embed/index.test.ts
Normal file
61
bin/mods/make-ai-embed/index.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { TestWorkingDirectory } from '$test/utils/twd';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { makeAiEmbed } from '.';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
process.env.TESTING = 'true';
|
||||
|
||||
const twd = new TestWorkingDirectory();
|
||||
|
||||
describe('Mods: make-ai-embed', () => {
|
||||
beforeAll(async () => {
|
||||
await twd.setup();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await twd.cleanup();
|
||||
});
|
||||
|
||||
it('should make embed source page', async () => {
|
||||
const aiComponent = path.join(twd.TWD, 'src/lib/ai2svelte/map.svelte');
|
||||
fs.copyFileSync(
|
||||
path.join(twd.TWD, 'src/lib/ai2svelte/ai-chart.svelte'),
|
||||
aiComponent
|
||||
);
|
||||
|
||||
await makeAiEmbed(aiComponent, 'en');
|
||||
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'pages/embeds/en/map/+page.svelte'))
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'pages/embeds/en/map/+page.server.ts'))
|
||||
).toBe(true);
|
||||
const pageContent = fs.readFileSync(
|
||||
path.join(twd.TWD, 'pages/embeds/en/map/+page.svelte'),
|
||||
'utf-8'
|
||||
);
|
||||
expect(pageContent).toMatch(
|
||||
`import Graphic from '$lib/ai2svelte/map.svelte';`
|
||||
);
|
||||
});
|
||||
|
||||
it('should build the app without error', async () => {
|
||||
try {
|
||||
execSync('vite build');
|
||||
} catch {
|
||||
expect(false).toBe(true);
|
||||
}
|
||||
expect(true).toBe(true);
|
||||
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'dist/embeds/en/map/index.html'))
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'dist/embeds/en/page/index.html'))
|
||||
).toBe(true);
|
||||
expect(fs.existsSync(path.join(twd.TWD, 'dist/index.html'))).toBe(true);
|
||||
}, 30_000);
|
||||
});
|
||||
100
bin/mods/make-ai-embed/index.ts
Normal file
100
bin/mods/make-ai-embed/index.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { globSync } from 'glob';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { getLocations } from '../_utils/locations';
|
||||
import { cancel, isCancel, log, select } from '@clack/prompts';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
import { utils } from '@reuters-graphics/graphics-bin';
|
||||
import * as url from 'url';
|
||||
import { note } from '@reuters-graphics/clack';
|
||||
import dedent from 'dedent';
|
||||
import c from 'picocolors';
|
||||
import { Mod } from '../_utils/mod';
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const mod = new Mod();
|
||||
|
||||
const promptForAiComponent = async () => {
|
||||
const { ROOT } = getLocations();
|
||||
|
||||
const aiComponents = globSync('*.svelte', {
|
||||
cwd: path.join(ROOT, 'src/lib/ai2svelte'),
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
const aiComponent = await select({
|
||||
message:
|
||||
'Which of these ai2svelte components do you want to create an embed for?',
|
||||
options: aiComponents.map((filePath) => ({
|
||||
label: path.basename(filePath, '.svelte'),
|
||||
value: filePath,
|
||||
})),
|
||||
});
|
||||
if (isCancel(aiComponent)) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
return aiComponent;
|
||||
};
|
||||
|
||||
const promptForLocale = async () => {
|
||||
const locale = await select({
|
||||
message: "What's the language for this graphic embed?",
|
||||
initialValue: 'en',
|
||||
options: [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'ar', label: 'Arabic' },
|
||||
{ value: 'fr', label: 'French' },
|
||||
{ value: 'es', label: 'Spanish' },
|
||||
{ value: 'de', label: 'German' },
|
||||
{ value: 'it', label: 'Italian' },
|
||||
{ value: 'ja', label: 'Japanese' },
|
||||
{ value: 'pt', label: 'Portugese' },
|
||||
{ value: 'ru', label: 'Russian' },
|
||||
],
|
||||
});
|
||||
if (isCancel(locale)) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
return locale;
|
||||
};
|
||||
|
||||
export const makeAiEmbed = async (aiComponent?: string, locale?: string) => {
|
||||
const { ROOT } = getLocations();
|
||||
|
||||
if (!aiComponent) aiComponent = await promptForAiComponent();
|
||||
if (!locale) locale = await promptForLocale();
|
||||
|
||||
if (!fs.existsSync(aiComponent)) return;
|
||||
if (!locale) return;
|
||||
|
||||
const aiSlug = slugify(path.basename(aiComponent, '.svelte'));
|
||||
|
||||
const pagesDir = path.join(ROOT, 'pages/embeds', locale, aiSlug);
|
||||
const componentPath = path.join(pagesDir, '+page.svelte');
|
||||
const loaderPath = path.join(pagesDir, '+page.server.ts');
|
||||
|
||||
if (fs.existsSync(componentPath)) {
|
||||
log.error('An embed already exists for this ai2svelte component');
|
||||
return;
|
||||
}
|
||||
utils.fs.ensureDir(componentPath);
|
||||
|
||||
mod.fs.copy([__dirname, 'templates/+page.svelte'], componentPath);
|
||||
|
||||
mod
|
||||
.magicFile(componentPath)
|
||||
.replaceAll('ai-chart.svelte', path.basename(aiComponent))
|
||||
.saveFile();
|
||||
|
||||
mod.fs.copy([__dirname, 'templates/+page.server.ts'], loaderPath);
|
||||
|
||||
if (!process.env.TESTING)
|
||||
log.info(`Embed created: ${path.relative(ROOT, componentPath)}`);
|
||||
if (!process.env.TESTING)
|
||||
note(dedent`Be sure to add this graphic to your ${c.cyan('"embeds"')} ArchieML
|
||||
doc and export AI statics for it before publishing.
|
||||
`);
|
||||
};
|
||||
3
bin/mods/make-ai-embed/templates/$types.d.ts
vendored
Normal file
3
bin/mods/make-ai-embed/templates/$types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* eslint-disable */
|
||||
export type PageServerLoad = any;
|
||||
export type PageProps = any;
|
||||
19
bin/mods/make-ai-embed/templates/+page.server.ts
Normal file
19
bin/mods/make-ai-embed/templates/+page.server.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
import { story } from '$locales/en/embeds.json';
|
||||
|
||||
interface Embed {
|
||||
locale: string;
|
||||
slug: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = ({ route }) => {
|
||||
const embed = (story.embeds as Embed[]).find(({ locale, slug }) => {
|
||||
return route.id === `/embeds/${locale?.trim()}/${slug?.trim()}`;
|
||||
});
|
||||
|
||||
return { embed };
|
||||
};
|
||||
91
bin/mods/make-ai-embed/templates/+page.svelte
Normal file
91
bin/mods/make-ai-embed/templates/+page.svelte
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import { assets } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
Theme,
|
||||
Article,
|
||||
PymChild,
|
||||
SEO,
|
||||
GraphicBlock,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import LogBlock from '$lib/components/dev/LogBlock.svelte';
|
||||
import Graphic from '$lib/ai2svelte/ai-chart.svelte';
|
||||
|
||||
// Styles
|
||||
import '@reuters-graphics/graphics-components/scss/main.scss';
|
||||
import '$lib/styles/global.scss';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let { embed } = data;
|
||||
</script>
|
||||
|
||||
<SEO
|
||||
baseUrl={import.meta.env.BASE_URL}
|
||||
pageUrl={page.url}
|
||||
seoTitle=""
|
||||
seoDescription=""
|
||||
shareTitle=""
|
||||
shareDescription=""
|
||||
shareImgPath={embed ?
|
||||
`${assets}/images/embeds/${embed.locale?.trim()}/${embed.slug?.trim()}.jpg`
|
||||
: `${assets}/images/reuters-graphics.jpg`}
|
||||
/>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
</svelte:head>
|
||||
|
||||
<Theme base="light">
|
||||
<Article
|
||||
embedded={true}
|
||||
columnWidths={{
|
||||
narrower: 330,
|
||||
narrow: 510,
|
||||
normal: 708,
|
||||
wide: 930,
|
||||
wider: 1076,
|
||||
}}
|
||||
>
|
||||
{#if !embed}
|
||||
<LogBlock level="warn" message="Missing embed in ArchieML doc" />
|
||||
{/if}
|
||||
{#if embed && !embed?.altText}
|
||||
<LogBlock level="warn" message="Missing altText in embeds ArchieML doc" />
|
||||
{/if}
|
||||
<GraphicBlock
|
||||
class="!my-0"
|
||||
width="normal"
|
||||
textWidth="normal"
|
||||
snap={false}
|
||||
title={embed?.title}
|
||||
description={embed?.description}
|
||||
notes={embed?.notes}
|
||||
ariaDescription={embed?.altText}
|
||||
>
|
||||
<Graphic assetsPath={assets} />
|
||||
</GraphicBlock>
|
||||
</Article>
|
||||
</Theme>
|
||||
|
||||
<PymChild polling={500} />
|
||||
|
||||
<style>
|
||||
:global(article.embedded) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
:global(.article-block.notes) {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
@media (max-width: 1023.98px) {
|
||||
:global(.article-block.graphic) {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
}
|
||||
:global(body) {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
:global(h3) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
111
bin/mods/project-type/index.test.ts
Normal file
111
bin/mods/project-type/index.test.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { TestWorkingDirectory } from '$test/utils/twd';
|
||||
import { changeProjectType } from '.';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import * as url from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
process.env.TESTING = 'true';
|
||||
|
||||
const twd = new TestWorkingDirectory();
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const templatesDir = path.join(__dirname, 'templates');
|
||||
const pagePlusTemplates = path.join(templatesDir, 'page+embed');
|
||||
const embedTemplates = path.join(templatesDir, 'embed-only');
|
||||
|
||||
describe('Mods: project-type', () => {
|
||||
beforeAll(async () => {
|
||||
await twd.setup();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await twd.cleanup();
|
||||
});
|
||||
|
||||
it('should change project type to embeds-only', async () => {
|
||||
await changeProjectType(true);
|
||||
// Removes page embed
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'pages/embeds/en/page/+page.svelte'))
|
||||
).toBe(false);
|
||||
// Rewrites homepage
|
||||
expect(
|
||||
fs.readFileSync(path.join(twd.TWD, 'pages/+page.svelte'), 'utf8')
|
||||
).toMatch('<meta name="robots" content="noindex, nofollow" />');
|
||||
// Rewrites publisher config
|
||||
expect(
|
||||
fs.readFileSync(path.join(twd.TWD, 'publisher.config.ts'), 'utf8')
|
||||
).toMatch('locales/en/embeds.json?story.authors');
|
||||
// Archives pages+ templates
|
||||
expect(fs.existsSync(path.join(pagePlusTemplates, '+page.svelte'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(pagePlusTemplates, 'embeds/en/page/+page.svelte'))
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should build embeds-only pages', async () => {
|
||||
try {
|
||||
execSync('vite build');
|
||||
} catch {
|
||||
expect(false).toBe(true);
|
||||
}
|
||||
expect(true).toBe(true);
|
||||
|
||||
expect(fs.existsSync(path.join(twd.TWD, 'dist/index.html'))).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'dist/embeds/en/page/index.html'))
|
||||
).toBe(false);
|
||||
}, 30_000);
|
||||
|
||||
it('should change project type back to pages+', async () => {
|
||||
await changeProjectType(true);
|
||||
|
||||
// Brings back page embed
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'pages/embeds/en/page/+page.svelte'))
|
||||
).toBe(true);
|
||||
|
||||
// Archives embeds+ template
|
||||
expect(fs.existsSync(path.join(embedTemplates, '+page.svelte'))).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(embedTemplates, 'publisher.config.ts'))
|
||||
).toBe(true);
|
||||
|
||||
// Removes pages+ templates
|
||||
expect(fs.existsSync(path.join(pagePlusTemplates, '+page.svelte'))).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(pagePlusTemplates, 'embeds/en/page/+page.svelte'))
|
||||
).toBe(false);
|
||||
|
||||
// Rewrites homepage
|
||||
expect(
|
||||
fs.readFileSync(path.join(twd.TWD, 'pages/+page.svelte'), 'utf8')
|
||||
).not.toMatch('<meta name="robots" content="noindex, nofollow" />');
|
||||
// Rewrites publisher config
|
||||
expect(
|
||||
fs.readFileSync(path.join(twd.TWD, 'publisher.config.ts'), 'utf8')
|
||||
).toMatch('locales/en/content.json?story.seoTitle');
|
||||
});
|
||||
|
||||
it('should build pages+ pages', async () => {
|
||||
await fs.remove(path.join(twd.TWD, 'dist'));
|
||||
try {
|
||||
execSync('vite build');
|
||||
} catch {
|
||||
expect(false).toBe(true);
|
||||
}
|
||||
expect(true).toBe(true);
|
||||
|
||||
expect(fs.existsSync(path.join(twd.TWD, 'dist/index.html'))).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(twd.TWD, 'dist/embeds/en/page/index.html'))
|
||||
).toBe(true);
|
||||
}, 30_000);
|
||||
});
|
||||
77
bin/mods/project-type/index.ts
Normal file
77
bin/mods/project-type/index.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import * as url from 'url';
|
||||
import { cancel, confirm, isCancel } from '@clack/prompts';
|
||||
import c from 'picocolors';
|
||||
import { getLocations } from '../_utils/locations';
|
||||
import { Mod } from '../_utils/mod';
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const templateDirs = {
|
||||
embedsOnly: path.join(__dirname, 'templates/embed-only'),
|
||||
pagesPlus: path.join(__dirname, 'templates/page+embed'),
|
||||
};
|
||||
|
||||
const mod = new Mod();
|
||||
|
||||
const changeToEmbedOnly = () => {
|
||||
const { ROOT } = getLocations();
|
||||
const pagesDir = path.join(ROOT, 'pages');
|
||||
|
||||
mod.fs.swap(
|
||||
[templateDirs.embedsOnly, '+page.svelte'],
|
||||
[pagesDir, '+page.svelte'],
|
||||
[templateDirs.pagesPlus, '+page.svelte']
|
||||
);
|
||||
mod.fs.move(
|
||||
[pagesDir, 'embeds/en/page/+page.svelte'],
|
||||
[templateDirs.pagesPlus, 'embeds/en/page/+page.svelte']
|
||||
);
|
||||
mod.fs.swap(
|
||||
[templateDirs.embedsOnly, 'publisher.config.ts'],
|
||||
[ROOT, 'publisher.config.ts'],
|
||||
[templateDirs.pagesPlus, 'publisher.config.ts']
|
||||
);
|
||||
};
|
||||
|
||||
const changeToPagesPlus = () => {
|
||||
const { ROOT } = getLocations();
|
||||
const pagesDir = path.join(ROOT, 'pages');
|
||||
|
||||
mod.fs.swap(
|
||||
[templateDirs.pagesPlus, '+page.svelte'],
|
||||
[pagesDir, '+page.svelte'],
|
||||
[templateDirs.embedsOnly, '+page.svelte']
|
||||
);
|
||||
mod.fs.move(
|
||||
[templateDirs.pagesPlus, 'embeds/en/page/+page.svelte'],
|
||||
[pagesDir, 'embeds/en/page/+page.svelte']
|
||||
);
|
||||
mod.fs.swap(
|
||||
[templateDirs.pagesPlus, 'publisher.config.ts'],
|
||||
[ROOT, 'publisher.config.ts'],
|
||||
[templateDirs.embedsOnly, 'publisher.config.ts']
|
||||
);
|
||||
};
|
||||
|
||||
export const changeProjectType = async (force = false) => {
|
||||
const isEmbedOnly = fs.existsSync(
|
||||
path.join(templateDirs.pagesPlus, '+page.svelte')
|
||||
);
|
||||
const typeLabel = isEmbedOnly ? 'pages + embeds' : 'embeds-only';
|
||||
|
||||
if (!force) {
|
||||
const confirmed = await confirm({
|
||||
message: `Are you sure you want to change your project type to ${c.cyan(typeLabel)}?`,
|
||||
});
|
||||
|
||||
if (isCancel(confirmed) || !confirmed) return cancel();
|
||||
}
|
||||
|
||||
if (isEmbedOnly) {
|
||||
changeToPagesPlus();
|
||||
} else {
|
||||
changeToEmbedOnly();
|
||||
}
|
||||
};
|
||||
30
bin/mods/project-type/templates/embed-only/+page.svelte
Normal file
30
bin/mods/project-type/templates/embed-only/+page.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { SEO } from '@reuters-graphics/graphics-components';
|
||||
|
||||
import { assets } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { isReutersDotcom, isReutersPreview } from '$utils/env';
|
||||
</script>
|
||||
|
||||
<SEO
|
||||
baseUrl={import.meta.env.BASE_URL}
|
||||
pageUrl={page.url}
|
||||
seoTitle=""
|
||||
seoDescription=""
|
||||
shareTitle=""
|
||||
shareDescription=""
|
||||
shareImgPath={`${assets}/images/reuters-graphics.jpg`}
|
||||
/>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
{#if !isReutersDotcom(page.url) || isReutersPreview(page.url)}
|
||||
<meta http-equiv="refresh" content="0; URL='./embed-previewer/'" />
|
||||
{:else}
|
||||
<meta
|
||||
http-equiv="refresh"
|
||||
content="0; URL='https://www.reuters.com/graphics/'"
|
||||
/>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { defineConfig } from '@reuters-graphics/graphics-kit-publisher';
|
||||
|
||||
export default defineConfig({
|
||||
metadataPointers: {
|
||||
pack: {
|
||||
rootSlug: false,
|
||||
wildSlug: false,
|
||||
title: false,
|
||||
byline: 'locales/en/embeds.json?story.authors',
|
||||
},
|
||||
},
|
||||
publishingLocations: [
|
||||
{
|
||||
archive: 'public',
|
||||
availableLocations: {
|
||||
lynx: false,
|
||||
connect: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
archive: /media-\w{2}-page/,
|
||||
availableLocations: {
|
||||
lynx: false,
|
||||
connect: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
archiveEditions: {
|
||||
docs: {
|
||||
'README.txt': 'bin/connect-docs/README.txt',
|
||||
'EMBED.txt': 'bin/connect-docs/EMBED.txt',
|
||||
},
|
||||
},
|
||||
});
|
||||
33
bin/mods/rngs-io/index.test.ts
Normal file
33
bin/mods/rngs-io/index.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
||||
import { TestWorkingDirectory } from '$test/utils/twd';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { unconfigRngsIo } from '.';
|
||||
import dedent from 'dedent';
|
||||
|
||||
process.env.TESTING = 'true';
|
||||
|
||||
const twd = new TestWorkingDirectory();
|
||||
|
||||
describe('Mods: unconfig-rngs-io', () => {
|
||||
beforeAll(async () => {
|
||||
await twd.setup();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await twd.cleanup();
|
||||
});
|
||||
|
||||
it('should overwrite layout.ts', async () => {
|
||||
unconfigRngsIo();
|
||||
|
||||
expect(fs.existsSync(path.join(twd.TWD, 'pages/+layout.ts'))).toBe(true);
|
||||
expect(fs.readFileSync(path.join(twd.TWD, 'pages/+layout.ts'), 'utf8'))
|
||||
.toMatch(dedent`import enContent from '$locales/en/content.json';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return { content: enContent.story };
|
||||
};
|
||||
`);
|
||||
});
|
||||
});
|
||||
16
bin/mods/rngs-io/index.ts
Normal file
16
bin/mods/rngs-io/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { getLocations } from '../_utils/locations';
|
||||
import * as url from 'url';
|
||||
import path from 'path';
|
||||
import { Mod } from '../_utils/mod';
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
const templatesDir = path.join(__dirname, 'templates');
|
||||
|
||||
const mod = new Mod();
|
||||
|
||||
export const unconfigRngsIo = () => {
|
||||
const { ROOT } = getLocations();
|
||||
const pagesDir = path.join(ROOT, 'pages');
|
||||
|
||||
mod.fs.copy([templatesDir, '+layout.ts'], [pagesDir, '+layout.ts']);
|
||||
};
|
||||
2
bin/mods/rngs-io/templates/$types.d.ts
vendored
Normal file
2
bin/mods/rngs-io/templates/$types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/* eslint-disable */
|
||||
export type LayoutLoad = any;
|
||||
9
bin/mods/rngs-io/templates/+layout.ts
Normal file
9
bin/mods/rngs-io/templates/+layout.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
|
||||
import type { LayoutLoad } from './$types.js';
|
||||
import enContent from '$locales/en/content.json';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return { content: enContent.story };
|
||||
};
|
||||
79
bin/svelte-kit/plugins/svelte-kit-pages/index.ts
Normal file
79
bin/svelte-kit/plugins/svelte-kit-pages/index.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import fs from 'fs';
|
||||
import { glob } from 'glob';
|
||||
import { normalizePath } from 'vite';
|
||||
import path from 'path';
|
||||
import type { ViteDevServer } from 'vite';
|
||||
|
||||
const getPkgRoot = () => {
|
||||
const PKG_PATH = path.join(process.cwd(), 'package.json');
|
||||
if (!fs.existsSync(PKG_PATH)) {
|
||||
throw new Error(
|
||||
'Unable to find package.json in your current working directory. Are you running from the root of your project?'
|
||||
);
|
||||
}
|
||||
return process.cwd();
|
||||
};
|
||||
|
||||
export default function svelteKitPagesPlugin({
|
||||
base = '/',
|
||||
pages = 'pages',
|
||||
} = {}) {
|
||||
const VIRTUAL_MODULE_ID = '@svelte-kit-pages';
|
||||
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
||||
const PAGES_DIR = path.join(getPkgRoot(), pages);
|
||||
|
||||
let FOUND_PAGES: string[] = [];
|
||||
|
||||
// Gets paths to pages
|
||||
const getPagePaths = () => {
|
||||
const pages = glob.sync('**/*.svelte', { cwd: PAGES_DIR });
|
||||
// Reset FOUND_PAGES
|
||||
FOUND_PAGES = pages.map((embed) =>
|
||||
normalizePath(path.join(PAGES_DIR, embed))
|
||||
);
|
||||
// Remove Svelte-specific extensions
|
||||
const pagePaths = pages.map((embed) => {
|
||||
const pagePath = path.join(base, embed.replace('.svelte', ''));
|
||||
return /\+page$/.test(pagePath) ?
|
||||
pagePath.replace(/\+page$/, '')
|
||||
: pagePath;
|
||||
});
|
||||
// Return as virtual module
|
||||
return `export default ['${pagePaths.join("', '")}'];`;
|
||||
};
|
||||
|
||||
const reloadModule = (server: ViteDevServer) => {
|
||||
const plugin = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
|
||||
if (!plugin) return;
|
||||
server.moduleGraph.invalidateModule(plugin);
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'svelte-kit-pages-plugin',
|
||||
|
||||
resolveId(id: string) {
|
||||
if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
|
||||
},
|
||||
|
||||
load(id: string) {
|
||||
if (id === RESOLVED_VIRTUAL_MODULE_ID) return getPagePaths();
|
||||
},
|
||||
|
||||
configureServer(server: ViteDevServer) {
|
||||
server.watcher.add(normalizePath(PAGES_DIR));
|
||||
server.watcher.on('unlink', (f) => {
|
||||
const file = normalizePath(f);
|
||||
if (FOUND_PAGES.includes(file)) reloadModule(server);
|
||||
});
|
||||
},
|
||||
|
||||
handleHotUpdate({ server, file }) {
|
||||
// If a page is added and not already in pages we've found, reload the plugin...
|
||||
if (file.includes(PAGES_DIR) && !FOUND_PAGES.includes(file)) {
|
||||
reloadModule(server);
|
||||
server.watcher.add(normalizePath(file));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
29
eslint.config.js
Normal file
29
eslint.config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { svelte } from '@reuters-graphics/yaks-eslint';
|
||||
|
||||
/**
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
files: [
|
||||
'bin/**/*.{js,ts}',
|
||||
'src/**/*.{js,ts,svelte}',
|
||||
'pages/**/*.{js,ts,svelte}',
|
||||
],
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/',
|
||||
'docs/',
|
||||
'.changeset/',
|
||||
'.svelte-kit/',
|
||||
'.astro/',
|
||||
],
|
||||
},
|
||||
...svelte,
|
||||
{
|
||||
rules: {
|
||||
'svelte3/unused-export-let': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
4
google.json
Normal file
4
google.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"docs": {},
|
||||
"sheets": {}
|
||||
}
|
||||
8
lefthook.yml
Normal file
8
lefthook.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pre-commit:
|
||||
scripts:
|
||||
'precheck-file-size.js':
|
||||
runner: node
|
||||
commands:
|
||||
prettier:
|
||||
glob: '*.{js,css,md,svelte}'
|
||||
run: npx prettier --write {staged_files} && git add .
|
||||
75
locales/en/content.json
Normal file
75
locales/en/content.json
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"metadata": {
|
||||
"id": "cltmvzj5m0000lc089jz22aet",
|
||||
"name": "graphics-kit-page",
|
||||
"storyboard": {
|
||||
"id": "cltmvxt5q0000l908irus4rdd",
|
||||
"name": "🔒 TEMPLATES"
|
||||
},
|
||||
"lastSynced": "2025-03-26T17:16:03.466Z",
|
||||
"liveEditing": {
|
||||
"preview": {
|
||||
"aml": "https://graphics.thomsonreuters.com/apps/graphics-tools/prod/stories/cltmvzj5m0000lc089jz22aet/preview/story.aml",
|
||||
"json": "https://graphics.thomsonreuters.com/apps/graphics-tools/prod/stories/cltmvzj5m0000lc089jz22aet/preview/story.json",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"story": {
|
||||
"slug": "ROOT-SLUG/WILD",
|
||||
"seoTitle": "Page title for search",
|
||||
"seoDescription": "Page description for search",
|
||||
"shareTitle": "Page title for social media",
|
||||
"shareDescription": "Page description for social media",
|
||||
"shareImgPath": "images/reuters-graphics.jpg",
|
||||
"shareImgAlt": "Alt text for share image.",
|
||||
"hed": "A Reuters Graphics page",
|
||||
"section": "Graphics",
|
||||
"sectionUrl": "https://www.reuters.com/graphics/",
|
||||
"authors": ["Jane Doe", "John Doe"],
|
||||
"publishTime": "2024-04-17T17:00:00.000Z",
|
||||
"updateTime": "",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Pig short ribs jerky, meatloaf turducken ribeye strip steak bacon pastrami tail pancetta chicken. Turkey landjaeger <a href=\"https://www.reuters.com/graphics/\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">kevin turducken kielbasa</a> pork loin, filet mignon leberkas meatball cow tenderloin. T-bone meatloaf kielbasa pancetta filet mignon doner. \r\n\r\nPig short ribs jerky, meatloaf <em>turducken ribeye strip steak</em> bacon pastrami tail pancetta chicken. Shankle pork loin burgdoggen, prosciutto beef ribs turducken ball tip.\r\n\r\nBoudin alcatra kevin, jerky swine <strong>brisket pastrami</strong> tail pork chop drumstick tongue."
|
||||
},
|
||||
{
|
||||
"type": "ai-graphic",
|
||||
"chart": "AiMap",
|
||||
"width": "normal",
|
||||
"textWidth": "normal",
|
||||
"title": "Optional title of the graphic",
|
||||
"description": "Optional chatter describes more about the graphic.",
|
||||
"notes": "Note: Optional note clarifying something in the data.\r\n\r\nSource: Optional source of the data.",
|
||||
"altText": "Add a description of the graphic for screen readers. This is invisible on the page."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Quis ea deserunt tempor. Incididunt ipsum culpa laboris. Aliqua culpa excepteur magna. Fugiat aliqua commodo ea ut dolor. Irure deserunt qui commodo sunt elit deserunt culpa. Amet occaecat ipsum cupidatat proident incididunt officia non ea.\r\n\r\nId incididunt laboris cillum eu. Ullamco ipsum mollit ipsum consequat laboris. Pariatur incididunt sit aute nulla enim excepteur ut."
|
||||
},
|
||||
{
|
||||
"type": "inline-ad",
|
||||
"n": "1"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Dolor qui dolore nulla pariatur sunt fugiat. Fugiat amet voluptate consequat. Dolore consequat proident commodo. Exercitation occaecat deserunt occaecat ea. Reprehenderit incididunt ad quis proident laboris et dolore.\r\n\r\nSint velit elit do sunt ad voluptate nulla dolore. Deserunt aliquip mollit reprehenderit nulla do ad quis ullamco. Deserunt aliquip aute qui eiusmod aliquip ullamco nostrud incididunt."
|
||||
}
|
||||
],
|
||||
"endNotes": [
|
||||
{
|
||||
"title": "Note",
|
||||
"text": "Data is current as of today"
|
||||
},
|
||||
{
|
||||
"title": "Sources",
|
||||
"text": "Data, Inc."
|
||||
},
|
||||
{
|
||||
"title": "Edited by",
|
||||
"text": "Editor, Copyeditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
14
locales/en/embeds.json
Normal file
14
locales/en/embeds.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"metadata": {
|
||||
"id": "cm8q3vr0e0000l803tfleu4t4",
|
||||
"name": "graphics-kit-embeds",
|
||||
"storyboard": {
|
||||
"id": "cltmvxt5q0000l908irus4rdd",
|
||||
"name": "🔒 TEMPLATES"
|
||||
},
|
||||
"lastSynced": "2025-03-26T17:16:03.468Z"
|
||||
},
|
||||
"story": {
|
||||
"embeds": []
|
||||
}
|
||||
}
|
||||
0
media-assets/en/.gitkeep
Normal file
0
media-assets/en/.gitkeep
Normal file
111
package.json
Normal file
111
package.json
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"name": "hypnagaga",
|
||||
"version": "2.2.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "vite dev --open",
|
||||
"stories:sync": "npx rngs-io stories sync",
|
||||
"preview": "npm-run-all publish:preview stories:autolink publish:git-commit",
|
||||
"upload": "npx graphics-publisher upload",
|
||||
"upload:quick": "npx graphics-publisher upload:quick",
|
||||
"pub": "npm-run-all publish:publish stories:autolink publish:git-commit",
|
||||
"build": "vite build",
|
||||
"build:preview": "PREVIEW=true vite build",
|
||||
"mods": "tsx ./bin/mods/index.ts mods",
|
||||
"savile": "savile row ./src/statics/images",
|
||||
"stories:unconfig": "tsx ./bin/mods/index.ts unconfig-rngs-io",
|
||||
"stories:new": "npx rngs-io stories new --template",
|
||||
"stories:connect": "npx rngs-io stories connect",
|
||||
"stories:autolink": "npx rngs-io stories autolink --name page-en",
|
||||
"stories:live": "npx rngs-io stories live preview",
|
||||
"get-google": "npx graphics google:get-docs google.json",
|
||||
"lock-google": "npx graphics google:lock-docs google.json",
|
||||
"publish:preview": "npx graphics-publisher preview",
|
||||
"publish:publish": "npx graphics-publisher publish",
|
||||
"publish:git-commit": "npx graphics github:push",
|
||||
"startup:check-creds": "npx graphics dotfile:check",
|
||||
"startup:create-repo": "npx graphics github:create-repo",
|
||||
"startup": "npm-run-all startup:*",
|
||||
"test": "vitest",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"lint": "eslint --fix .",
|
||||
"format": "prettier . --write",
|
||||
"knip": "knip"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/starlight": "^0.32.2",
|
||||
"@changesets/cli": "^2.27.10",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@evilmartians/lefthook": "^1.11.5",
|
||||
"@reuters-graphics/clack": "^0.0.2",
|
||||
"@reuters-graphics/graphics-bin": "^1.1.8",
|
||||
"@reuters-graphics/graphics-kit-publisher": "^3.1.2",
|
||||
"@reuters-graphics/illustrator-exports": "^0.0.2",
|
||||
"@reuters-graphics/rngs-io-client": "^0.1.12",
|
||||
"@reuters-graphics/savile": "^0.0.4",
|
||||
"@reuters-graphics/vite-plugin-purge-styles": "^0.0.3",
|
||||
"@reuters-graphics/yaks-eslint": "^0.1.1",
|
||||
"@reuters-graphics/yaks-prettier": "^0.1.1",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.20.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/language-tags": "^1.0.4",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"astro": "^5.5.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cheerio": "^1.0.0",
|
||||
"dedent": "^1.5.3",
|
||||
"empathic": "^1.0.0",
|
||||
"eslint": "^9.21.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"glob": "^11.0.0",
|
||||
"ignore": "^7.0.3",
|
||||
"knip": "^5.37.2",
|
||||
"magic-string": "^0.30.17",
|
||||
"mock-fs": "^5.5.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"sade": "^1.8.1",
|
||||
"sass": "^1.86.0",
|
||||
"sharp": "^0.33.5",
|
||||
"svelte": "^5.36.13",
|
||||
"svelte-check": "^4.3.0",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"vitest": "^3.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@evilmartians/lefthook",
|
||||
"@fortawesome/fontawesome-common-types",
|
||||
"@fortawesome/free-regular-svg-icons",
|
||||
"@fortawesome/free-solid-svg-icons",
|
||||
"@parcel/watcher",
|
||||
"@sveltejs/kit",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"svelte-preprocess"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@reuters-graphics/graphics-components": "^3.2.1",
|
||||
"language-tags": "^1.0.9"
|
||||
},
|
||||
"reuters": {
|
||||
"graphic": {},
|
||||
"preview": ""
|
||||
},
|
||||
"homepage": ""
|
||||
}
|
||||
16
pages/+layout.ts
Normal file
16
pages/+layout.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
|
||||
import type { LayoutLoad } from './$types.js';
|
||||
import { LiveEndpoints } from '../src/utils/liveEndpoints';
|
||||
import rngsioConfig from '../rngs-io.json';
|
||||
import enContent from '$locales/en/content.json';
|
||||
|
||||
export const load: LayoutLoad = async ({ url }) => {
|
||||
const liveEndpoints = new LiveEndpoints(rngsioConfig, url);
|
||||
const { story: content } = await liveEndpoints.getLiveContent(
|
||||
'en/content',
|
||||
enContent
|
||||
);
|
||||
return { content };
|
||||
};
|
||||
64
pages/+page.svelte
Normal file
64
pages/+page.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
AdScripts,
|
||||
SEO,
|
||||
SiteHeader,
|
||||
SiteFooter,
|
||||
EmbedPreviewerLink,
|
||||
LeaderboardAd,
|
||||
Theme,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import App from '$lib/App.svelte';
|
||||
import pkg from '$pkg';
|
||||
import { dev } from '$app/environment';
|
||||
import { assets } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { isReutersApp, isReutersDev, isReutersDotcom } from '$utils/env';
|
||||
import LogBlock from '$lib/components/dev/LogBlock.svelte';
|
||||
|
||||
// Styles
|
||||
import '@reuters-graphics/graphics-components/scss/main.scss';
|
||||
import '$lib/styles/global.scss';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
let content = $derived(data.content);
|
||||
</script>
|
||||
|
||||
{#if isReutersDotcom(page.url)}
|
||||
<AdScripts />
|
||||
{/if}
|
||||
|
||||
<SEO
|
||||
baseUrl={import.meta.env.BASE_URL}
|
||||
pageUrl={page.url}
|
||||
seoTitle={content.seoTitle}
|
||||
seoDescription={content.seoDescription}
|
||||
shareTitle={content.shareTitle}
|
||||
shareDescription={content.shareDescription}
|
||||
shareImgPath={`${assets}/${content.shareImgPath}`}
|
||||
shareImgAlt={content.shareImgAlt}
|
||||
publishTime={pkg?.reuters?.graphic?.published}
|
||||
updateTime={pkg?.reuters?.graphic?.updated}
|
||||
authors={pkg?.reuters?.graphic?.authors}
|
||||
/>
|
||||
|
||||
<Theme base="light">
|
||||
{#if !isReutersApp(page.url)}
|
||||
{#if isReutersDotcom(page.url)}
|
||||
<LeaderboardAd />
|
||||
{:else if isReutersDev(page.url)}
|
||||
<LogBlock level="info" message="An ad will appear here on dotcom" />
|
||||
{/if}
|
||||
<SiteHeader />
|
||||
{/if}
|
||||
|
||||
<App {content} />
|
||||
|
||||
{#if !isReutersApp(page.url)}
|
||||
<SiteFooter />
|
||||
{/if}
|
||||
</Theme>
|
||||
|
||||
<!-- Only visible in dev! -->
|
||||
<EmbedPreviewerLink {dev} />
|
||||
46
pages/embed-previewer/+page.svelte
Normal file
46
pages/embed-previewer/+page.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import pages from '@svelte-kit-pages';
|
||||
import { Theme, Framer } from '@reuters-graphics/graphics-components';
|
||||
import { base } from '$app/paths';
|
||||
import '@reuters-graphics/graphics-components/scss/main.scss';
|
||||
|
||||
const embeds = pages
|
||||
.filter((p) => /^\/embeds\//.test(p))
|
||||
.map((path) => `${base}/${path.replace(/^\//, '')}`)
|
||||
.map((path) => (/\/$/.test(path) ? path : path + '/'));
|
||||
</script>
|
||||
|
||||
<Theme>
|
||||
{#if embeds.length}
|
||||
<Framer {embeds} />
|
||||
{:else}
|
||||
<container>
|
||||
<div>
|
||||
<div>No embeds found.</div>
|
||||
<div>
|
||||
<a href="{base}/">Go back</a>
|
||||
</div>
|
||||
</div>
|
||||
</container>
|
||||
{/if}
|
||||
</Theme>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@reuters-graphics/graphics-components/dist/scss/mixins' as mixins;
|
||||
|
||||
:global(body) {
|
||||
padding-bottom: 60px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80svh;
|
||||
width: 100%;
|
||||
& > div {
|
||||
text-align: center;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
pages/embeds/en/page/+page.svelte
Normal file
50
pages/embeds/en/page/+page.svelte
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import pkg from '$pkg';
|
||||
import { assets } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Theme, PymChild, SEO } from '@reuters-graphics/graphics-components';
|
||||
|
||||
import archieML from '$locales/en/content.json';
|
||||
|
||||
// App or embeddable chart component
|
||||
import App from '$lib/App.svelte';
|
||||
|
||||
// Styles
|
||||
import '@reuters-graphics/graphics-components/scss/main.scss';
|
||||
import '$lib/styles/global.scss';
|
||||
|
||||
let content = $derived(archieML.story);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
</svelte:head>
|
||||
|
||||
<SEO
|
||||
baseUrl={import.meta.env.BASE_URL}
|
||||
pageUrl={page.url}
|
||||
seoTitle={content.seoTitle}
|
||||
seoDescription={content.seoDescription}
|
||||
shareTitle={content.shareTitle}
|
||||
shareDescription={content.shareDescription}
|
||||
shareImgPath={`${assets}/${content.shareImgPath}`}
|
||||
shareImgAlt={content.shareImgAlt}
|
||||
publishTime={pkg?.reuters?.graphic?.published}
|
||||
updateTime={pkg?.reuters?.graphic?.updated}
|
||||
authors={pkg?.reuters?.graphic?.authors}
|
||||
/>
|
||||
|
||||
<Theme base="light">
|
||||
<App embedded={true} {content} />
|
||||
</Theme>
|
||||
|
||||
<PymChild polling={500} />
|
||||
|
||||
<style>
|
||||
:global(.headline-container) {
|
||||
margin-top: 0;
|
||||
}
|
||||
:global(.headline) {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
14766
pnpm-lock.yaml
Normal file
14766
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
4590
project-files/ai2html.ait
Normal file
4590
project-files/ai2html.ait
Normal file
File diff suppressed because one or more lines are too long
2521
project-files/dotcom.ait
Normal file
2521
project-files/dotcom.ait
Normal file
File diff suppressed because one or more lines are too long
2483
project-files/sharecard.ait
Normal file
2483
project-files/sharecard.ait
Normal file
File diff suppressed because one or more lines are too long
34
publisher.config.ts
Normal file
34
publisher.config.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { defineConfig } from '@reuters-graphics/graphics-kit-publisher';
|
||||
|
||||
export default defineConfig({
|
||||
metadataPointers: {
|
||||
pack: {
|
||||
rootSlug: false,
|
||||
wildSlug: false,
|
||||
title: 'locales/en/content.json?story.seoTitle',
|
||||
},
|
||||
},
|
||||
publishingLocations: [
|
||||
{
|
||||
archive: 'public',
|
||||
availableLocations: {
|
||||
lynx: false,
|
||||
connect: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
archive: /media-\w{2}-page/,
|
||||
availableLocations: {
|
||||
lynx: false,
|
||||
connect: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
archiveEditions: {
|
||||
docs: {
|
||||
'README.txt': 'bin/connect-docs/README.txt',
|
||||
'EMBED.txt': 'bin/connect-docs/EMBED.txt',
|
||||
},
|
||||
ignore: ['project-files/'],
|
||||
},
|
||||
});
|
||||
52
src/global.d.ts
vendored
Normal file
52
src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
declare module '@svelte-kit-pages' {
|
||||
const embeds: string[];
|
||||
export default embeds;
|
||||
}
|
||||
|
||||
declare module '$pkg' {
|
||||
import type { Graphic, RNGS } from '@reuters-graphics/server-client';
|
||||
|
||||
type EditionType =
|
||||
| 'interactive'
|
||||
| 'media-interactive'
|
||||
| 'JPG'
|
||||
| 'EPS'
|
||||
| 'PNG'
|
||||
| 'PDF';
|
||||
|
||||
type PkgArchive = {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
uploaded: string;
|
||||
editions: EditionType[];
|
||||
};
|
||||
|
||||
export const name: string;
|
||||
export const version: string;
|
||||
export const description: string;
|
||||
export const homepage: string;
|
||||
export const reuters: {
|
||||
preview?: string;
|
||||
separateAssets?: string;
|
||||
graphic?: {
|
||||
slugs?: {
|
||||
root: string;
|
||||
wild: string;
|
||||
};
|
||||
language?: RNGS.Language;
|
||||
desk?: Graphic.Desk;
|
||||
pack?: string;
|
||||
contactEmail?: string;
|
||||
authors?: {
|
||||
name: string;
|
||||
link: string;
|
||||
}[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
published?: string;
|
||||
updated?: string;
|
||||
archives?: Record<string, PkgArchive>;
|
||||
};
|
||||
};
|
||||
}
|
||||
17
src/hooks.server.ts
Normal file
17
src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { RequestEvent, Handle } from '@sveltejs/kit';
|
||||
import tags from 'language-tags';
|
||||
|
||||
const getLang = (event: RequestEvent) => {
|
||||
// lang is encoded in URL pathname
|
||||
const matches = /\/([a-z]{2})\//g.exec(event.url.pathname);
|
||||
if (!matches) return 'en';
|
||||
if (matches[1] && tags.check(matches[1])) return matches[1];
|
||||
return 'en';
|
||||
};
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return resolve(event, {
|
||||
// Set lang attribute in html
|
||||
transformPageChunk: ({ html }) => html.replace('%lang%', getLang(event)),
|
||||
});
|
||||
};
|
||||
97
src/lib/App.svelte
Normal file
97
src/lib/App.svelte
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<script lang="ts">
|
||||
import type ArchieML from '$locales/en/content.json';
|
||||
|
||||
interface Props {
|
||||
/* Whether the app is being rendered in an embed */
|
||||
embedded?: boolean;
|
||||
/* ArchieML story content */
|
||||
content: (typeof ArchieML)['story'];
|
||||
}
|
||||
|
||||
let { embedded = false, content }: Props = $props();
|
||||
|
||||
import { assets } from '$app/paths';
|
||||
|
||||
import {
|
||||
Article,
|
||||
Analytics,
|
||||
BodyText,
|
||||
EndNotes,
|
||||
SiteHeadline,
|
||||
GraphicBlock,
|
||||
InlineAd,
|
||||
} from '@reuters-graphics/graphics-components';
|
||||
import LogBlock from './components/dev/LogBlock.svelte';
|
||||
import { containerWidth, inlineAdNumber } from '$utils/propValidators';
|
||||
import { isReutersDotcom } from '$utils/env';
|
||||
import { page } from '$app/state';
|
||||
import pkg from '$pkg';
|
||||
|
||||
// Import ai2svelte components...
|
||||
import AiMap from './ai2svelte/ai-chart.svelte';
|
||||
|
||||
// ...and add them to this object.
|
||||
const aiCharts = {
|
||||
AiMap,
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !embedded && isReutersDotcom(page.url)}
|
||||
<Analytics authors={pkg?.reuters?.graphic?.authors || []} />
|
||||
{/if}
|
||||
|
||||
<Article>
|
||||
<!--
|
||||
This component and others are part of our components library.
|
||||
📚 Read the docs: https://reuters-graphics.github.io/graphics-components/
|
||||
-->
|
||||
<SiteHeadline
|
||||
hed={content.hed}
|
||||
section={content.section}
|
||||
sectionUrl={content.sectionUrl}
|
||||
authors={content.authors}
|
||||
publishTime={content.publishTime}
|
||||
updateTime={content.updateTime}
|
||||
/>
|
||||
|
||||
<!-- 🔁 Looping through your ArchieML doc blocks... -->
|
||||
{#each content.blocks as block}
|
||||
<!-- Text block -->
|
||||
{#if block.type === 'text'}
|
||||
<BodyText text={block.text} />
|
||||
|
||||
<!-- Ai2svelte graphic block -->
|
||||
{:else if block.type === 'ai-graphic'}
|
||||
{#if !aiCharts[block.chart]}
|
||||
<LogBlock message={`Unable to find "${block.chart}" in aiCharts`} />
|
||||
{:else}
|
||||
{@const AiChart = aiCharts[block.chart]}
|
||||
<GraphicBlock
|
||||
id={block.chart}
|
||||
width={containerWidth(block.width)}
|
||||
textWidth={containerWidth(block.textWidth)}
|
||||
title={block.title}
|
||||
description={block.description}
|
||||
notes={block.notes}
|
||||
ariaDescription={block.altText}
|
||||
>
|
||||
<AiChart assetsPath={assets || '/'} />
|
||||
</GraphicBlock>
|
||||
{/if}
|
||||
|
||||
<!-- Inline ad -->
|
||||
{:else if block.type === 'inline-ad'}
|
||||
{#if isReutersDotcom(page.url)}
|
||||
<InlineAd n={inlineAdNumber(block.n)} />
|
||||
{:else}
|
||||
<LogBlock level="info" message="An ad will appear here on dotcom" />
|
||||
{/if}
|
||||
|
||||
<!-- Warning block -->
|
||||
{:else}
|
||||
<LogBlock message={`Unknown block type: "${block.type}"`} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<EndNotes notes={content.endNotes} />
|
||||
</Article>
|
||||
98
src/lib/ai2svelte/ai-chart.svelte
Normal file
98
src/lib/ai2svelte/ai-chart.svelte
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<script>
|
||||
export let assetsPath = '/';
|
||||
let width = null;
|
||||
</script>
|
||||
|
||||
<!-- Generated by ai2html v0.100.0 - 2021-09-30 13:49 -->
|
||||
|
||||
<div id="g-ai-chart-box" bind:clientWidth={width}>
|
||||
<!-- Artboard: xs -->
|
||||
{#if width && width >= 0 && width < 510}
|
||||
<div id="g-ai-chart-xs" class="g-artboard" style="">
|
||||
<div style="padding: 0 0 91.7004% 0;"></div>
|
||||
<div
|
||||
id="g-ai-chart-xs-img"
|
||||
class="g-aiImg"
|
||||
alt=""
|
||||
style="background-image: url({assetsPath.replace(
|
||||
new RegExp('/([^/.]+)$'),
|
||||
'/$1/'
|
||||
)}images/graphics/ai-chart-xs.png);"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Artboard: sm -->
|
||||
{#if width && width >= 510 && width < 660}
|
||||
<div id="g-ai-chart-sm" class="g-artboard" style="">
|
||||
<div style="padding: 0 0 82.703% 0;"></div>
|
||||
<div
|
||||
id="g-ai-chart-sm-img"
|
||||
class="g-aiImg"
|
||||
alt=""
|
||||
style="background-image: url({assetsPath.replace(
|
||||
new RegExp('/([^/.]+)$'),
|
||||
'/$1/'
|
||||
)}images/graphics/ai-chart-sm.png);"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Artboard: md -->
|
||||
{#if width && width >= 660}
|
||||
<div id="g-ai-chart-md" class="g-artboard" style="">
|
||||
<div style="padding: 0 0 79.6009% 0;"></div>
|
||||
<div
|
||||
id="g-ai-chart-md-img"
|
||||
class="g-aiImg"
|
||||
alt=""
|
||||
style="background-image: url({assetsPath.replace(
|
||||
new RegExp('/([^/.]+)$'),
|
||||
'/$1/'
|
||||
)}images/graphics/ai-chart-md.png);"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- End ai2html - 2021-09-30 13:49 -->
|
||||
|
||||
<!-- ai file: ai-chart.ai -->
|
||||
|
||||
<!-- svelte-ignore css-unused-selector -->
|
||||
<style lang="scss">
|
||||
#g-ai-chart-box,
|
||||
#g-ai-chart-box .g-artboard {
|
||||
margin: 0 auto;
|
||||
}
|
||||
#g-ai-chart-box p {
|
||||
margin: 0;
|
||||
}
|
||||
#g-ai-chart-box .g-aiAbs {
|
||||
position: absolute;
|
||||
}
|
||||
#g-ai-chart-box .g-aiImg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#g-ai-chart-box .g-aiPointText p {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#g-ai-chart-xs {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-ai-chart-sm {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#g-ai-chart-md {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Custom CSS */
|
||||
</style>
|
||||
29
src/lib/components/ExampleComponent.svelte
Normal file
29
src/lib/components/ExampleComponent.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { Block } from '@reuters-graphics/graphics-components';
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
<section class="graphic">
|
||||
<Block>
|
||||
<h5>The count is <span>{count}</span></h5>
|
||||
<button
|
||||
class="rounded cursor-pointer py-2.5 px-5 uppercase text-secondary font-bold"
|
||||
onclick={() => {
|
||||
count += 1;
|
||||
}}>Add</button
|
||||
>
|
||||
</Block>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@reuters-graphics/graphics-components/dist/scss/mixins' as mixins;
|
||||
|
||||
section.graphic {
|
||||
h5 {
|
||||
color: blue;
|
||||
span {
|
||||
@include mixins.font-mono;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
src/lib/components/dev/LogBlock.svelte
Normal file
64
src/lib/components/dev/LogBlock.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { isReutersDev } from '$utils/env';
|
||||
import { Block } from '@reuters-graphics/graphics-components';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* A message to display
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* The level of the message, either `warn` or `info`
|
||||
*/
|
||||
level?: 'warn' | 'info';
|
||||
}
|
||||
|
||||
let { message, level = 'warn' }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if isReutersDev(page.url)}
|
||||
<!-- This log will only appear in development. -->
|
||||
<Block>
|
||||
<div class={level}>
|
||||
<span>{level.toUpperCase()}</span>
|
||||
{message}
|
||||
</div>
|
||||
</Block>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
margin: 15px 0;
|
||||
padding: 0.75rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 4px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.warn {
|
||||
color: #000;
|
||||
background-color: #ffe0e0;
|
||||
border: 1px solid #ffbebe;
|
||||
span {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: #000;
|
||||
background-color: #e0f7ff;
|
||||
border: 1px solid #d2dcff;
|
||||
span {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/lib/styles/global.scss
Normal file
4
src/lib/styles/global.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// 🌐 This is a global stylesheet.
|
||||
// You can use it to write styles for any elements on pages or embeds
|
||||
// and to override any defaults that are part of our house design system.
|
||||
@use '@reuters-graphics/graphics-components/scss/mixins' as mixins;
|
||||
BIN
src/statics/images/graphics/ai-chart-md.png
Normal file
BIN
src/statics/images/graphics/ai-chart-md.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 606 KiB |
BIN
src/statics/images/graphics/ai-chart-sm.png
Normal file
BIN
src/statics/images/graphics/ai-chart-sm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 376 KiB |
BIN
src/statics/images/graphics/ai-chart-xs.png
Normal file
BIN
src/statics/images/graphics/ai-chart-xs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
1
src/statics/images/manifest.json
Normal file
1
src/statics/images/manifest.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{ "shark.jpg": { "width": 1400, "height": 788, "size": 103 } }
|
||||
BIN
src/statics/images/reuters-graphics.jpg
Normal file
BIN
src/statics/images/reuters-graphics.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
48
src/template.html
Normal file
48
src/template.html
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!doctype html>
|
||||
<html lang="%lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta http-equiv="cleartype" content="on" />
|
||||
<link
|
||||
href="https://www.reuters.com/pf/resources/images/reuters/favicon/tr_fvcn_kinesis_32x32_v2.ico?d=227"
|
||||
rel="icon"
|
||||
sizes="any"
|
||||
/>
|
||||
<link
|
||||
href="https://www.reuters.com/pf/resources/images/reuters/favicon/tr_kinesis_v2.svg?d=227"
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
/>
|
||||
<link
|
||||
href="https://www.reuters.com/pf/resources/images/reuters/favicon/tr_fvcn_kinesis_180x180_v2.png?d=227"
|
||||
rel="apple-touch-icon"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://graphics.thomsonreuters.com/style-assets/fonts/v1/Knowledge2017-Bold.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://graphics.thomsonreuters.com/style-assets/fonts/v1/Knowledge2017-Medium.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://graphics.thomsonreuters.com/style-assets/fonts/v1/Knowledge2017-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte-app">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
72
src/utils/env/index.ts
vendored
Normal file
72
src/utils/env/index.ts
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { building } from '$app/environment';
|
||||
|
||||
/**
|
||||
* Check if the page is being hosted inside the Reuters mobile app.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { page } from "$app/stores";
|
||||
*
|
||||
* isReutersApp($page.url);
|
||||
* ```
|
||||
* @param url URL of current page
|
||||
* @returns `true` if in the Reuters app
|
||||
*/
|
||||
export const isReutersApp = (url: URL) => {
|
||||
if (building) return false;
|
||||
return url.searchParams.get('outputType') === 'chromeless';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the page is being hosted on reuters.com.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { page } from "$app/stores";
|
||||
*
|
||||
* isReutersDotcom($page.url);
|
||||
* ```
|
||||
*
|
||||
* @param url URL of current page
|
||||
* @returns `true` if on reuters.com and not in an iframe
|
||||
*/
|
||||
export const isReutersDotcom = (url: URL) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (window.self !== window.top) return false;
|
||||
}
|
||||
return /\Wreuters\.com$/.test(url.hostname);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the page is being hosted on our preview server at graphics.thomsonreuters.com.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { page } from "$app/stores";
|
||||
*
|
||||
* isReutersPreview($page.url);
|
||||
* ```
|
||||
*
|
||||
* @param url URL of current page
|
||||
* @returns `true` if on graphics.thomsonreuters.com
|
||||
*/
|
||||
export const isReutersPreview = (url: URL) => {
|
||||
return url.hostname === 'graphics.thomsonreuters.com';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the page is being hosted in development or on our preview server at graphics.thomsonreuters.com.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { page } from "$app/stores";
|
||||
*
|
||||
* isReutersDev($page.url);
|
||||
* ```
|
||||
*
|
||||
* @param url URL of current page
|
||||
* @returns `true` if on dev or graphics.thomsonreuters.com
|
||||
*/
|
||||
export const isReutersDev = (url: URL) => {
|
||||
return url.hostname === 'localhost' || isReutersPreview(url);
|
||||
};
|
||||
197
src/utils/liveEndpoints/index.ts
Normal file
197
src/utils/liveEndpoints/index.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
interface ConfigLiveEndpoint {
|
||||
enabled: boolean;
|
||||
json: string;
|
||||
aml: string;
|
||||
}
|
||||
|
||||
interface StoryConfig {
|
||||
name: string;
|
||||
rngsIo: string;
|
||||
syncPath: string;
|
||||
preview?: ConfigLiveEndpoint;
|
||||
public?: ConfigLiveEndpoint;
|
||||
}
|
||||
|
||||
interface StoryboardConfig {
|
||||
name: string;
|
||||
rngsIo: string;
|
||||
stories: {
|
||||
[storyId: string]: StoryConfig;
|
||||
};
|
||||
}
|
||||
|
||||
interface StoryClientConfig {
|
||||
storyboards: {
|
||||
[storyboardId: string]: StoryboardConfig;
|
||||
};
|
||||
}
|
||||
|
||||
type LiveEndpoint = {
|
||||
localFile: string;
|
||||
metadata: {
|
||||
id: string;
|
||||
name: string;
|
||||
storyboard: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
lastPublished: string;
|
||||
version: 'preview' | 'public';
|
||||
};
|
||||
story: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* LiveEndpoints connects your app to updating data published independent of your project files.
|
||||
*
|
||||
* This is often used in preview stages, but can also be used in production to allow updating a
|
||||
* published page with new content.
|
||||
*/
|
||||
export class LiveEndpoints {
|
||||
private clientConfig: StoryClientConfig;
|
||||
private currentUrl: URL;
|
||||
/**
|
||||
* @param clientConfig RNGS.io config object, usually imported directly from `rngs-io.json`
|
||||
* in the root of the project.
|
||||
* @param currentUrl URL of the current page.
|
||||
*/
|
||||
constructor(clientConfig: StoryClientConfig, currentUrl: URL) {
|
||||
this.clientConfig = clientConfig;
|
||||
this.currentUrl = currentUrl;
|
||||
}
|
||||
|
||||
private extractLiveUrlsFromConfig(config: StoryClientConfig) {
|
||||
const urls: {
|
||||
url: string;
|
||||
localFile: string;
|
||||
version: 'preview' | 'public';
|
||||
}[] = [];
|
||||
|
||||
// Loop through each storyboard in the storyboards object
|
||||
for (const storyboardId in config.storyboards) {
|
||||
const storyboard = config.storyboards[storyboardId];
|
||||
|
||||
// Loop through each story in the stories object of the current storyboard
|
||||
for (const storyId in storyboard.stories) {
|
||||
const story = storyboard.stories[storyId];
|
||||
|
||||
// Check both 'preview' and 'public' keys in the story
|
||||
(['preview', 'public'] as const).forEach((version) => {
|
||||
const media = story[version];
|
||||
// If media exists, is not an empty object, and enabled is true
|
||||
if (media && Object.keys(media).length !== 0 && media.enabled) {
|
||||
// If the json URL exists, add it to the urls array
|
||||
if (media.json) {
|
||||
urls.push({
|
||||
localFile: story.syncPath,
|
||||
url: media.json,
|
||||
version,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
private async fetchLiveUrls(urlsData: { url: string; localFile: string }[]) {
|
||||
const fetchPromises = urlsData.map(
|
||||
async (data: {
|
||||
url: string;
|
||||
localFile: string;
|
||||
}): Promise<LiveEndpoint> => {
|
||||
const response = await fetch(data.url);
|
||||
const story = (await response.json()) as LiveEndpoint;
|
||||
return { ...story, localFile: data.localFile };
|
||||
}
|
||||
);
|
||||
|
||||
const result = await Promise.all(fetchPromises);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async fetchLiveEndPoints() {
|
||||
const liveUrls = this.extractLiveUrlsFromConfig(this.clientConfig);
|
||||
|
||||
let liveStories: LiveEndpoint[] = [];
|
||||
|
||||
// Public pages
|
||||
if (this.currentUrl.hostname === 'reuters.com') {
|
||||
const versionLiveUrls = liveUrls.filter(
|
||||
({ version }) => version === 'public'
|
||||
);
|
||||
liveStories = await this.fetchLiveUrls(versionLiveUrls);
|
||||
}
|
||||
|
||||
// Preview pages
|
||||
if (this.currentUrl.hostname === 'graphics.thomsonreuters.com') {
|
||||
const versionLiveUrls = liveUrls.filter(
|
||||
({ version }) => version === 'preview'
|
||||
);
|
||||
liveStories = await this.fetchLiveUrls(versionLiveUrls);
|
||||
}
|
||||
|
||||
return liveStories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version of content docs, which may be published remotely.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import localContent from '$locales/en/content.json';
|
||||
*
|
||||
* const liveEndpoints = new LiveEndpoints(rngsIoConfig, url);
|
||||
* const content = await liveEndpoints.getLiveContent('en/content', localContent);
|
||||
* ```
|
||||
* @param localeFilePath Path part of the locale file, e.g., `en/content` for
|
||||
* `locales/en/content.json`
|
||||
* @param localContent Local copy of the locale file, imported directly as json.
|
||||
* @returns The latest content
|
||||
*/
|
||||
async getLiveContent<T>(localeFilePath: string, localContent: T): Promise<T> {
|
||||
const liveEndpoints = await this.fetchLiveEndPoints();
|
||||
const pathParts = localeFilePath.split('/');
|
||||
if (pathParts.length !== 2) {
|
||||
throw new Error(
|
||||
'Invalid locale file. Must have just two path parts, e.g., "en/content"'
|
||||
);
|
||||
}
|
||||
|
||||
const liveContent = liveEndpoints.find(
|
||||
(endPoint) => endPoint.localFile === `locales/${localeFilePath}.json`
|
||||
);
|
||||
|
||||
// If no live content found, return local content
|
||||
if (!liveContent) {
|
||||
return localContent;
|
||||
}
|
||||
|
||||
// Compare lastPublished dates to determine which content is more recent
|
||||
const localContentWithMetadata = localContent as {
|
||||
metadata?: { lastSynced?: string };
|
||||
};
|
||||
|
||||
const liveLastPublished = liveContent.metadata?.lastPublished;
|
||||
const localLastSynced = localContentWithMetadata.metadata?.lastSynced;
|
||||
|
||||
// If local content doesn't have lastSynced return live content
|
||||
if (!localLastSynced) {
|
||||
return liveContent as typeof localContent;
|
||||
}
|
||||
|
||||
// If live content doesn't have lastPublished, return local content
|
||||
if (!liveLastPublished) {
|
||||
return localContent;
|
||||
}
|
||||
|
||||
// Compare dates - return live content if it's more recent, otherwise return local
|
||||
const liveDate = new Date(liveLastPublished);
|
||||
const localDate = new Date(localLastSynced);
|
||||
|
||||
if (liveDate > localDate) return liveContent as typeof localContent;
|
||||
return localContent;
|
||||
}
|
||||
}
|
||||
68
src/utils/propValidators/index.ts
Normal file
68
src/utils/propValidators/index.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Coeerce a string to a valid container width.
|
||||
*
|
||||
* Used to satisfy type checking.
|
||||
*
|
||||
* @param width Width string
|
||||
* @returns valid container width
|
||||
*/
|
||||
export const containerWidth = (width: string) => {
|
||||
switch (width) {
|
||||
case 'narrower':
|
||||
case 'narrow':
|
||||
case 'normal':
|
||||
case 'wide':
|
||||
case 'wider':
|
||||
case 'widest':
|
||||
case 'fluid':
|
||||
return width;
|
||||
default:
|
||||
return 'normal'; // Default value if invalid
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate inline ad number prop
|
||||
* @param n string
|
||||
* @returns Ad number
|
||||
*/
|
||||
export const inlineAdNumber = (n: string) => {
|
||||
switch (n) {
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
return n;
|
||||
default:
|
||||
return '1';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Coerce a truth-y value to a proper boolean.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* truthyString('true'); // true
|
||||
* truthyString('f'); // false
|
||||
* truthrString('yes'); // true
|
||||
* truthyString('0'); // false
|
||||
* ```
|
||||
* @param value
|
||||
* @param defaultValue Default value
|
||||
* @returns `true` or `false`
|
||||
*/
|
||||
export const truthy = (value: string, defaultValue = false) => {
|
||||
// trim and standardise string
|
||||
const cleaned = value.toLowerCase().trim();
|
||||
|
||||
const truthyStrings = ['true', 't', 'yes', '1'];
|
||||
const falsyStrings = ['false', 'f', 'no', '0'];
|
||||
|
||||
if (truthyStrings.includes(cleaned)) {
|
||||
return true;
|
||||
} else if (falsyStrings.includes(cleaned)) {
|
||||
return false;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
71
svelte.config.js
Normal file
71
svelte.config.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import { sveltePreprocess } from 'svelte-preprocess';
|
||||
import { getBasePath } from '@reuters-graphics/graphics-kit-publisher';
|
||||
|
||||
const mode =
|
||||
process.env.TESTING ? 'test'
|
||||
: process.env.PREVIEW ? 'preview'
|
||||
: process.env.NODE_ENV === 'production' ? 'prod'
|
||||
: 'dev';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: sveltePreprocess({
|
||||
preserve: ['ld+json'],
|
||||
scss: { quietDeps: true, api: 'modern-compiler' },
|
||||
postcss: {
|
||||
plugins: [autoprefixer],
|
||||
},
|
||||
}),
|
||||
vitePlugin: {
|
||||
onwarn: (warning, handler) => {
|
||||
// Triggered by our use of SCSS mixins ...
|
||||
if (warning.code === 'vite-plugin-svelte-preprocess-many-dependencies')
|
||||
return;
|
||||
handler(warning);
|
||||
},
|
||||
experimental: {
|
||||
disableSvelteResolveWarnings: true,
|
||||
},
|
||||
},
|
||||
kit: {
|
||||
appDir: '_app',
|
||||
paths: {
|
||||
assets: getBasePath(mode, 'cdn', {
|
||||
trailingSlash: false,
|
||||
rootRelative: false,
|
||||
}),
|
||||
base: decodeURI(
|
||||
getBasePath(mode, {
|
||||
trailingSlash: false,
|
||||
rootRelative: true,
|
||||
})
|
||||
),
|
||||
},
|
||||
adapter: adapter({
|
||||
pages: 'dist',
|
||||
assets: 'dist/cdn',
|
||||
fallback: null,
|
||||
precompress: false,
|
||||
}),
|
||||
files: {
|
||||
assets: 'src/statics',
|
||||
lib: 'src/lib',
|
||||
routes: 'pages',
|
||||
appTemplate: 'src/template.html',
|
||||
},
|
||||
alias: {
|
||||
$lib: './src/lib',
|
||||
'$lib/*': './src/lib/*',
|
||||
'$utils/*': './src/utils/*',
|
||||
$pkg: './package.json',
|
||||
$images: './src/statics/images',
|
||||
'$images/*': './src/statics/images/*',
|
||||
'$locales/*': './locales/*',
|
||||
'$test/*': './test/*',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"include": [
|
||||
// .svelte-kit/tsconfig.json
|
||||
".svelte-kit/ambient.d.ts",
|
||||
".svelte-kit/non-ambient.d.ts",
|
||||
".svelte-kit/types/**/$types.d.ts",
|
||||
"vite.config.js",
|
||||
"vite.config.ts",
|
||||
"pages/**/*.js",
|
||||
"pages/**/*.ts",
|
||||
"pages/**/*.svelte",
|
||||
"src/**/*.js",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.svelte",
|
||||
"tests/**/*.js",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.svelte",
|
||||
// Mods
|
||||
"bin/mods/**/*.svelte",
|
||||
"bin/mods/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
28
vite.config.ts
Normal file
28
vite.config.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { purgeStyles } from '@reuters-graphics/vite-plugin-purge-styles';
|
||||
import svelteKitPagesPlugin from './bin/svelte-kit/plugins/svelte-kit-pages/';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: { target: 'es2015', sourcemap: true },
|
||||
server: {
|
||||
open: true,
|
||||
port: 3000,
|
||||
fs: {
|
||||
allow: ['.'],
|
||||
},
|
||||
},
|
||||
test: {
|
||||
fileParallelism: false,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: { scss: { quietDeps: true, api: 'modern-compiler' } },
|
||||
},
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
svelteKitPagesPlugin(),
|
||||
purgeStyles({
|
||||
safeFiles: ['src/lib/styles/**/*.scss'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
Loading…
Reference in a new issue