Initial commit from monorepo
This commit is contained in:
115
scripts/generate-stories.mjs
Normal file
115
scripts/generate-stories.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const componentsRoot = path.resolve(process.cwd(), 'app/components')
|
||||
const storyExtensions = ['.stories.ts', '.stories.js', '.stories.tsx', '.stories.jsx']
|
||||
|
||||
const titleCase = (str) =>
|
||||
str
|
||||
.split(/[-_/]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('/')
|
||||
|
||||
const toIdentifier = (str) =>
|
||||
str
|
||||
.split(/[^a-zA-Z0-9]+/)
|
||||
.filter(Boolean)
|
||||
.map((part, idx) => {
|
||||
const lower = part.toLowerCase()
|
||||
if (idx === 0) return lower.charAt(0).toUpperCase() + lower.slice(1)
|
||||
return part.charAt(0).toUpperCase() + part.slice(1)
|
||||
})
|
||||
.join('') || 'Component'
|
||||
|
||||
const hasStorySibling = async (dir, baseName) => {
|
||||
const files = await fs.readdir(dir)
|
||||
return files.some((file) => {
|
||||
const match = file.match(/^(.+)\.stories\.(t|j)sx?$/i)
|
||||
if (!match) return false
|
||||
return match[1] === baseName
|
||||
})
|
||||
}
|
||||
|
||||
const walkVueFiles = async (dir) => {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
const files = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await walkVueFiles(fullPath)))
|
||||
continue
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith('.vue')) {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
const buildStoryContent = (filePath) => {
|
||||
const baseName = path.basename(filePath, '.vue')
|
||||
const importPath = `./${path.basename(filePath)}`
|
||||
const relative = path.relative(componentsRoot, filePath).replace(/\\/g, '/')
|
||||
const title = titleCase(relative.replace('.vue', ''))
|
||||
const componentId = toIdentifier(baseName)
|
||||
|
||||
return `import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import StoryComponent from '${importPath}'
|
||||
|
||||
const meta: Meta<typeof StoryComponent> = {
|
||||
title: '${title}',
|
||||
component: StoryComponent,
|
||||
render: (args) => ({
|
||||
components: { StoryComponent },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<StoryComponent v-bind=\"args\" />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const ensureDir = async (dir) => {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const vueFiles = await walkVueFiles(componentsRoot)
|
||||
const generated = []
|
||||
|
||||
for (const filePath of vueFiles) {
|
||||
const dir = path.dirname(filePath)
|
||||
const baseName = path.basename(filePath, '.vue')
|
||||
const storyExists = await hasStorySibling(dir, baseName)
|
||||
if (storyExists) continue
|
||||
|
||||
const storyPath = path.join(dir, `${baseName}.stories.ts`)
|
||||
await ensureDir(dir)
|
||||
const content = buildStoryContent(filePath)
|
||||
await fs.writeFile(storyPath, content, 'utf8')
|
||||
generated.push(path.relative(process.cwd(), storyPath))
|
||||
}
|
||||
|
||||
if (generated.length === 0) {
|
||||
console.log('No new stories generated. All components already have stories.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Generated ${generated.length} stories:`)
|
||||
generated.forEach((p) => console.log(`- ${p}`))
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error('Failed to generate stories', err)
|
||||
process.exit(1)
|
||||
})
|
||||
48
scripts/load-secrets.mjs
Normal file
48
scripts/load-secrets.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Load secrets from Infisical using Machine Identity (Universal Auth)
|
||||
* Writes secrets to .env.infisical file for sourcing
|
||||
*/
|
||||
import { InfisicalSDK } from "@infisical/sdk";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
const INFISICAL_API_URL = process.env.INFISICAL_API_URL;
|
||||
const INFISICAL_CLIENT_ID = process.env.INFISICAL_CLIENT_ID;
|
||||
const INFISICAL_CLIENT_SECRET = process.env.INFISICAL_CLIENT_SECRET;
|
||||
const INFISICAL_PROJECT_ID = process.env.INFISICAL_PROJECT_ID;
|
||||
const INFISICAL_ENV = process.env.INFISICAL_ENV || "prod";
|
||||
|
||||
if (!INFISICAL_API_URL || !INFISICAL_CLIENT_ID || !INFISICAL_CLIENT_SECRET || !INFISICAL_PROJECT_ID) {
|
||||
process.stderr.write("Missing required Infisical environment variables\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new InfisicalSDK({ siteUrl: INFISICAL_API_URL });
|
||||
|
||||
await client.auth().universalAuth.login({
|
||||
clientId: INFISICAL_CLIENT_ID,
|
||||
clientSecret: INFISICAL_CLIENT_SECRET,
|
||||
});
|
||||
|
||||
process.stderr.write(`Loading secrets from Infisical (env: ${INFISICAL_ENV})...\n`);
|
||||
|
||||
const envLines = [];
|
||||
|
||||
for (const secretPath of ["/webapp", "/shared"]) {
|
||||
const response = await client.secrets().listSecrets({
|
||||
projectId: INFISICAL_PROJECT_ID,
|
||||
environment: INFISICAL_ENV,
|
||||
secretPath: secretPath,
|
||||
expandSecretReferences: true,
|
||||
});
|
||||
|
||||
for (const secret of response.secrets) {
|
||||
// Escape special characters for shell
|
||||
const escapedValue = secret.secretValue.replace(/'/g, "'\\''");
|
||||
envLines.push(`export ${secret.secretKey}='${escapedValue}'`);
|
||||
}
|
||||
|
||||
process.stderr.write(` ${secretPath}: ${response.secrets.length} secrets loaded\n`);
|
||||
}
|
||||
|
||||
writeFileSync(".env.infisical", envLines.join("\n"));
|
||||
process.stderr.write("Secrets written to .env.infisical\n");
|
||||
Reference in New Issue
Block a user