0
Fork 0
mirror of https://github.com/penpot/penpot-export.git synced 2025-03-11 23:11:17 -05:00

feat(cli): allow to inspect Penpot URLs

This commit is contained in:
Roberto Redradix 2023-09-26 17:33:45 +02:00
parent 5cc7be703d
commit fe7691e6d1
5 changed files with 200 additions and 11 deletions

View file

@ -12,6 +12,8 @@
"scripts": { "scripts": {
"prepack": "npm run build", "prepack": "npm run build",
"build": "tsc", "build": "tsc",
"pretest": "npm run build",
"test": "node --test",
"format": "prettier -w .", "format": "prettier -w .",
"dev": "tsc-watch" "dev": "tsc-watch"
}, },

View file

@ -4,16 +4,61 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import penpotExport from '@penpot-export/core' import penpotExport from '@penpot-export/core'
const rootProjectPath = fs.realpathSync(process.cwd()) import { parsePenpotUrl } from '../penpot'
const configFilePath = path.resolve(rootProjectPath, 'penpot-export.config.js')
const exists = fs.existsSync(configFilePath)
if (!exists) { const [, , command, ...params] = process.argv
throw new Error(
'penpot-export: Config file not found. Check if file penpot-export.config.js exists at root.', switch (command) {
) case 'inspect': {
const [url] = params
if (!url) {
throw new Error(
'penpot-export: Missing URL to inspect. Provide an URL to the inspect command.',
)
}
try {
const parsed = parsePenpotUrl(url)
console.log(
[
'The following details are the result of inspecting the provided URL:',
`Penpot instance: ${parsed.instance}`,
`Workspace id: ${parsed.workspaceId}`,
`File id: ${parsed.fileId}`,
`Page id: ${parsed.pageId}`,
].join('\n\t'),
)
} catch (e) {
if (e instanceof TypeError) {
throw new Error(
`penpot-export: URL inspection failed with the following error: ${e.message}.`,
)
} else {
throw new Error(
'penpot-export: URL inspection failed with an unknown error.',
)
}
}
break
}
default: {
const rootProjectPath = fs.realpathSync(process.cwd())
const configFilePath = path.resolve(
rootProjectPath,
'penpot-export.config.js',
)
const exists = fs.existsSync(configFilePath)
if (!exists) {
throw new Error(
'penpot-export: Config file not found. Check if file penpot-export.config.js exists at root.',
)
}
const config = require(configFilePath)
penpotExport(config, rootProjectPath)
}
} }
const config = require(configFilePath)
penpotExport(config, rootProjectPath)

View file

@ -0,0 +1 @@
export * from './urls'

View file

@ -0,0 +1,115 @@
import assert from 'node:assert'
import { describe, it } from 'node:test'
import { parsePenpotUrl } from './urls'
describe('Penpot URL parser', () => {
describe('Params validation', () => {
describe('when provided an invalid URL', () => {
it('throws an error', () => {
const input = 'foo'
const expectedException = {
name: 'TypeError',
message: 'Invalid URL',
}
assert.throws(() => {
parsePenpotUrl(input)
}, expectedException)
})
})
describe('when provided a valid URL not from a Penpot file', () => {
it('throws an error', () => {
{
const input = 'https://design.penpot.app/#/settings/profile'
const expectedException = {
name: 'TypeError',
message: 'Invalid Penpot file URL',
}
assert.throws(() => {
parsePenpotUrl(input)
}, expectedException)
}
{
const input = 'https://design.penpot.app/#/settings/access-tokens'
const expectedException = {
name: 'TypeError',
message: 'Invalid Penpot file URL',
}
assert.throws(() => {
parsePenpotUrl(input)
}, expectedException)
}
})
})
})
describe('Parsing output', () => {
describe('when provided a Penpot SaaS URL', () => {
it('parses it without page-id param', () => {
const input =
'https://design.penpot.app/#/workspace/4a499800-872e-80e1-8002-fc0b4bbaa4e4/52961d58-0a92-80c2-8003-2e4b5e9a7826'
const expect = {
instance: 'https://design.penpot.app/',
workspaceId: '4a499800-872e-80e1-8002-fc0b4bbaa4e4',
fileId: '52961d58-0a92-80c2-8003-2e4b5e9a7826',
pageId: undefined,
}
assert.deepStrictEqual(parsePenpotUrl(input), expect)
})
it('parses it with page id', () => {
const input =
'https://design.penpot.app/#/workspace/4a499800-872e-80e1-8002-fc0b4bbaa4e4/52961d58-0a92-80c2-8003-2e4b5e9a7826?page-id=38f1e350-296d-80f1-8002-fd3314270d8c'
const expect = {
instance: 'https://design.penpot.app/',
workspaceId: '4a499800-872e-80e1-8002-fc0b4bbaa4e4',
fileId: '52961d58-0a92-80c2-8003-2e4b5e9a7826',
pageId: '38f1e350-296d-80f1-8002-fd3314270d8c',
}
assert.deepStrictEqual(parsePenpotUrl(input), expect)
})
})
describe('when provided a self-hosted Penpot URL', () => {
it('parses it without page-id param', () => {
const input =
'https://penpot.mydomain.com/#/workspace/da54e491-6ba3-11eb-9ba1-03f8ac143bbf/c86728dd-89fd-8169-8002-a71575166a74'
const expect = {
instance: 'https://penpot.mydomain.com/',
workspaceId: 'da54e491-6ba3-11eb-9ba1-03f8ac143bbf',
fileId: 'c86728dd-89fd-8169-8002-a71575166a74',
pageId: undefined,
}
assert.deepStrictEqual(parsePenpotUrl(input), expect)
})
it('parses it with page id', () => {
const input =
'https://penpot.mydomain.com/#/workspace/da54e491-6ba3-11eb-9ba1-03f8ac143bbf/c86728dd-89fd-8169-8002-a71575166a74?page-id=c86728dd-89fd-8169-8002-a71575166a75'
const expect = {
instance: 'https://penpot.mydomain.com/',
workspaceId: 'da54e491-6ba3-11eb-9ba1-03f8ac143bbf',
fileId: 'c86728dd-89fd-8169-8002-a71575166a74',
pageId: 'c86728dd-89fd-8169-8002-a71575166a75',
}
assert.deepStrictEqual(parsePenpotUrl(input), expect)
})
})
})
})

View file

@ -0,0 +1,26 @@
// NOTE A Penpot file URL has these forms as of v1.19:
// https://<origin>/#/workspace/<workspace-uuid>/<file-uuid>
// https://<origin>/#/workspace/<workspace-uuid>/<file-uuid>?page-id=<page-uuid>
const PENPOT_FILE_URL_HASH_RE = /#\/workspace\/.{36}\/.{36}(?:\?page-id=.{36})?/
const UUID_RE =
/^\p{Hex_Digit}{8}-\p{Hex_Digit}{4}-\p{Hex_Digit}{4}-\p{Hex_Digit}{4}-\p{Hex_Digit}{12}$/u
export function parsePenpotUrl(url: string) {
const parsedUrl = new URL(url)
if (!PENPOT_FILE_URL_HASH_RE.test(parsedUrl.hash)) {
throw new TypeError('Invalid Penpot file URL')
}
const [workspaceId, fileId, pageId] = parsedUrl.hash
.split(/[\/?=]/)
.filter((match) => UUID_RE.test(match))
return {
instance: parsedUrl.origin + parsedUrl.pathname,
workspaceId,
fileId,
pageId,
}
}