diff --git a/packages/adders/common.ts b/packages/adders/common.ts index a65ca1f9..a17c892d 100644 --- a/packages/adders/common.ts +++ b/packages/adders/common.ts @@ -65,10 +65,7 @@ export function addEslintConfigPrettier({ content }: FileEditor>, - path: string -): string { +export function addToDemoPage(content: string, path: string): string { const { template, generateCode } = parseSvelte(content); for (const node of template.ast.childNodes) { diff --git a/packages/adders/paraglide/index.ts b/packages/adders/paraglide/index.ts index 459f0295..3210c9fe 100644 --- a/packages/adders/paraglide/index.ts +++ b/packages/adders/paraglide/index.ts @@ -10,34 +10,18 @@ import { object, variables, exports, - kit + kit as kitJs } from '@sveltejs/cli-core/js'; import * as html from '@sveltejs/cli-core/html'; import { parseHtml, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers'; import { addToDemoPage } from '../common.ts'; -const DEFAULT_INLANG_PROJECT = { - $schema: 'https://inlang.com/schema/project-settings', - modules: [ - 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js', - 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js', - 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js', - 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js', - 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js', - 'https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js', - 'https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js' - ], - 'plugin.inlang.messageFormat': { - pathPattern: './messages/{languageTag}.json' - } -}; - export const options = defineAdderOptions({ availableLanguageTags: { question: `Which languages would you like to support? ${colors.gray('(e.g. en,de-ch)')}`, type: 'string', default: 'en', - validate(input: any) { + validate(input) { const { invalidLanguageTags, validLanguageTags } = parseLanguageTagInput(input); if (invalidLanguageTags.length > 0) { @@ -61,24 +45,35 @@ export const options = defineAdderOptions({ } }); +const DEFAULT_INLANG_PROJECT = { + $schema: 'https://inlang.com/schema/project-settings', + modules: [ + 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js', + 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js', + 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js', + 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js', + 'https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js', + 'https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js', + 'https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js' + ], + 'plugin.inlang.messageFormat': { + pathPattern: './messages/{languageTag}.json' + } +}; + export default defineAdder({ id: 'paraglide', environments: { svelte: false, kit: true }, homepage: 'https://inlang.com', options, - packages: [ - { - name: '@inlang/paraglide-sveltekit', - version: '^0.11.1', - dev: false - } - ], - files: [ - { - // create an inlang project if it doesn't exist yet - name: () => 'project.inlang/settings.json', - condition: ({ cwd }) => !fs.existsSync(path.join(cwd, 'project.inlang/settings.json')), - content: ({ options, content }) => { + run: ({ sv, cwd, options, typescript, kit, dependencyVersion }) => { + const ext = typescript ? 'ts' : 'js'; + if (!kit) throw new Error('SvelteKit is required'); + + sv.dependency('@inlang/paraglide-sveltekit', '^0.11.1'); + + if (!fs.existsSync(path.join(cwd, 'project.inlang/settings.json'))) { + sv.updateFile('project.inlang/settings.json', (content) => { const { data, generateCode } = parseJson(content); for (const key in DEFAULT_INLANG_PROJECT) { @@ -91,158 +86,135 @@ export default defineAdder({ data.languageTags = validLanguageTags; return generateCode(); - } - }, - { - // add the vite plugin - name: ({ typescript }) => `vite.config.${typescript ? 'ts' : 'js'}`, - content: ({ content }) => { - const { ast, generateCode } = parseScript(content); - - const vitePluginName = 'paraglide'; - imports.addNamed(ast, '@inlang/paraglide-sveltekit/vite', { paraglide: vitePluginName }); - - const { value: rootObject } = exports.defaultExport( - ast, - functions.call('defineConfig', []) - ); - const param1 = functions.argumentByIndex(rootObject, 0, object.createEmpty()); + }); + } - const pluginsArray = object.property(param1, 'plugins', array.createEmpty()); - const pluginFunctionCall = functions.call(vitePluginName, []); - const pluginConfig = object.create({ - project: common.createLiteral('./project.inlang'), - outdir: common.createLiteral('./src/lib/paraglide') - }); - functions.argumentByIndex(pluginFunctionCall, 0, pluginConfig); - array.push(pluginsArray, pluginFunctionCall); + // add the vite plugin + sv.updateFile(`vite.config.${ext}`, (content) => { + const { ast, generateCode } = parseScript(content); - return generateCode(); - } - }, - { - // src/lib/i18n file - name: ({ typescript }) => `src/lib/i18n.${typescript ? 'ts' : 'js'}`, - content({ content }) { - const { ast, generateCode } = parseScript(content); - - imports.addNamed(ast, '@inlang/paraglide-sveltekit', { createI18n: 'createI18n' }); - imports.addDefault(ast, '$lib/paraglide/runtime', '* as runtime'); - - const createI18nExpression = common.expressionFromString('createI18n(runtime)'); - const i18n = variables.declaration(ast, 'const', 'i18n', createI18nExpression); - - const existingExport = exports.namedExport(ast, 'i18n', i18n); - if (existingExport.declaration != i18n) { - log.warn('Setting up $lib/i18n failed because it already exports an i18n function'); - } + const vitePluginName = 'paraglide'; + imports.addNamed(ast, '@inlang/paraglide-sveltekit/vite', { paraglide: vitePluginName }); - return generateCode(); - } - }, - { - // reroute hook - name: ({ typescript }) => `src/hooks.${typescript ? 'ts' : 'js'}`, - content({ content }) { - const { ast, generateCode } = parseScript(content); - - imports.addNamed(ast, '$lib/i18n', { - i18n: 'i18n' - }); - - const expression = common.expressionFromString('i18n.reroute()'); - const rerouteIdentifier = variables.declaration(ast, 'const', 'reroute', expression); - - const existingExport = exports.namedExport(ast, 'reroute', rerouteIdentifier); - if (existingExport.declaration != rerouteIdentifier) { - log.warn('Adding the reroute hook automatically failed. Add it manually'); - } + const { value: rootObject } = exports.defaultExport(ast, functions.call('defineConfig', [])); + const param1 = functions.argumentByIndex(rootObject, 0, object.createEmpty()); - return generateCode(); + const pluginsArray = object.property(param1, 'plugins', array.createEmpty()); + const pluginFunctionCall = functions.call(vitePluginName, []); + const pluginConfig = object.create({ + project: common.createLiteral('./project.inlang'), + outdir: common.createLiteral('./src/lib/paraglide') + }); + functions.argumentByIndex(pluginFunctionCall, 0, pluginConfig); + array.push(pluginsArray, pluginFunctionCall); + + return generateCode(); + }); + + // src/lib/i18n file + sv.updateFile(`src/lib/i18n.${ext}`, (content) => { + const { ast, generateCode } = parseScript(content); + + imports.addNamed(ast, '@inlang/paraglide-sveltekit', { createI18n: 'createI18n' }); + imports.addDefault(ast, '$lib/paraglide/runtime', '* as runtime'); + + const createI18nExpression = common.expressionFromString('createI18n(runtime)'); + const i18n = variables.declaration(ast, 'const', 'i18n', createI18nExpression); + + const existingExport = exports.namedExport(ast, 'i18n', i18n); + if (existingExport.declaration !== i18n) { + log.warn('Setting up $lib/i18n failed because it already exports an i18n function'); } - }, - { - // handle hook - name: ({ typescript }) => `src/hooks.server.${typescript ? 'ts' : 'js'}`, - content({ content, typescript }) { - const { ast, generateCode } = parseScript(content); - imports.addNamed(ast, '$lib/i18n', { - i18n: 'i18n' - }); + return generateCode(); + }); - const hookHandleContent = 'i18n.handle()'; - kit.addHooksHandle(ast, typescript, 'handleParaglide', hookHandleContent); + // reroute hook + sv.updateFile(`src/hooks.${ext}`, (content) => { + const { ast, generateCode } = parseScript(content); - return generateCode(); + imports.addNamed(ast, '$lib/i18n', { i18n: 'i18n' }); + + const expression = common.expressionFromString('i18n.reroute()'); + const rerouteIdentifier = variables.declaration(ast, 'const', 'reroute', expression); + + const existingExport = exports.namedExport(ast, 'reroute', rerouteIdentifier); + if (existingExport.declaration !== rerouteIdentifier) { + log.warn('Adding the reroute hook automatically failed. Add it manually'); } - }, - { - // add the component to the layout - name: ({ kit }) => `${kit?.routesDirectory}/+layout.svelte`, - content: ({ content, dependencyVersion, typescript }) => { - const { script, template, generateCode } = parseSvelte(content, { typescript }); - const paraglideComponentName = 'ParaglideJS'; - imports.addNamed(script.ast, '@inlang/paraglide-sveltekit', { - [paraglideComponentName]: paraglideComponentName - }); - imports.addNamed(script.ast, '$lib/i18n', { - i18n: 'i18n' - }); + return generateCode(); + }); - if (template.source.length === 0) { - const svelteVersion = dependencyVersion('svelte'); - if (!svelteVersion) throw new Error('Failed to determine svelte version'); + // handle hook + sv.updateFile(`src/hooks.server.${ext}`, (content) => { + const { ast, generateCode } = parseScript(content); - html.addSlot(script.ast, template.ast, svelteVersion); - } + imports.addNamed(ast, '$lib/i18n', { i18n: 'i18n' }); - const templateCode = new MagicString(template.generateCode()); - if (!templateCode.original.includes('\n'); - templateCode.append('\n'); - } + const hookHandleContent = 'i18n.handle()'; + kitJs.addHooksHandle(ast, typescript, 'handleParaglide', hookHandleContent); + + return generateCode(); + }); + + // add the component to the layout + sv.updateFile(`${kit.routesDirectory}/+layout.svelte`, (content) => { + const { script, template, generateCode } = parseSvelte(content, { typescript }); + + const paraglideComponentName = 'ParaglideJS'; + imports.addNamed(script.ast, '@inlang/paraglide-sveltekit', { + [paraglideComponentName]: paraglideComponentName + }); + imports.addNamed(script.ast, '$lib/i18n', { i18n: 'i18n' }); - return generateCode({ script: script.generateCode(), template: templateCode.toString() }); + if (template.source.length === 0) { + const svelteVersion = dependencyVersion('svelte'); + if (!svelteVersion) throw new Error('Failed to determine svelte version'); + + html.addSlot(script.ast, template.ast, svelteVersion); } - }, - { - // add the text-direction and lang attribute placeholders to app.html - name: () => 'src/app.html', - content: ({ content }) => { - const { ast, generateCode } = parseHtml(content); - - const htmlNode = ast.children.find( - (child): child is html.HtmlElement => - child.type === html.HtmlElementType.Tag && child.name === 'html' - ); - if (!htmlNode) { - log.warn( - "Could not find node in app.html. You'll need to add the language placeholder manually" - ); - return generateCode(); - } - htmlNode.attribs = { - ...htmlNode.attribs, - lang: '%paraglide.lang%', - dir: '%paraglide.textDirection%' - }; + const templateCode = new MagicString(template.generateCode()); + if (!templateCode.original.includes('\n'); + templateCode.append('\n'); + } + + return generateCode({ script: script.generateCode(), template: templateCode.toString() }); + }); + + // add the text-direction and lang attribute placeholders to app.html + sv.updateFile('src/app.html', (content) => { + const { ast, generateCode } = parseHtml(content); + + const htmlNode = ast.children.find( + (child): child is html.HtmlElement => + child.type === html.HtmlElementType.Tag && child.name === 'html' + ); + if (!htmlNode) { + log.warn( + "Could not find node in app.html. You'll need to add the language placeholder manually" + ); return generateCode(); } - }, - { - name: ({ kit }) => `${kit?.routesDirectory}/demo/+page.svelte`, - condition: ({ options }) => options.demo, - content: (editor) => addToDemoPage(editor, 'paraglide') - }, - { + htmlNode.attribs = { + ...htmlNode.attribs, + lang: '%paraglide.lang%', + dir: '%paraglide.textDirection%' + }; + + return generateCode(); + }); + + if (options.demo) { + sv.updateFile(`${kit.routesDirectory}/demo/+page.svelte`, (content) => { + return addToDemoPage(content, 'paraglide'); + }); + // add usage example - name: ({ kit }) => `${kit?.routesDirectory}/demo/paraglide/+page.svelte`, - condition: ({ options }) => options.demo, - content({ content, options, typescript }) { + sv.updateFile(`${kit.routesDirectory}/demo/paraglide/+page.svelte`, (content) => { const { script, template, generateCode } = parseSvelte(content, { typescript }); imports.addDefault(script.ast, '$lib/paraglide/messages.js', '* as m'); @@ -265,15 +237,15 @@ export default defineAdder({ scriptCode.trim(); scriptCode.append('\n\n'); scriptCode.append(dedent` - ${ts('', '/**')} - ${ts('', '* @param import("$lib/paraglide/runtime").AvailableLanguageTag newLanguage')} - ${ts('', '*/')} - function switchToLanguage(newLanguage${ts(': AvailableLanguageTag')}) { - const canonicalPath = i18n.route($page.url.pathname); - const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage); - goto(localisedPath); - } - `); + ${ts('', '/**')} + ${ts('', '* @param import("$lib/paraglide/runtime").AvailableLanguageTag newLanguage')} + ${ts('', '*/')} + function switchToLanguage(newLanguage${ts(': AvailableLanguageTag')}) { + const canonicalPath = i18n.route($page.url.pathname); + const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage); + goto(localisedPath); + } + `); } const templateCode = new MagicString(template.source); @@ -293,26 +265,26 @@ export default defineAdder({ templateCode.append(`
\n${links}\n
`); return generateCode({ script: scriptCode.toString(), template: templateCode.toString() }); - } + }); } - ], - postInstall: ({ cwd, options }) => { - const jsonData: Record = {}; - jsonData['$schema'] = 'https://inlang.com/schema/inlang-message-format'; const { validLanguageTags } = parseLanguageTagInput(options.availableLanguageTags); for (const languageTag of validLanguageTags) { - jsonData.hello_world = `Hello, {name} from ${languageTag}!`; - - const filePath = `messages/${languageTag}.json`; - const directoryPath = path.dirname(filePath); - const fullDirectoryPath = path.join(cwd, directoryPath); - const fullFilePath = path.join(cwd, filePath); + sv.updateFile(`messages/${languageTag}.json`, (content) => { + const { data, generateCode } = parseJson(content); + data['$schema'] = 'https://inlang.com/schema/inlang-message-format'; + data.hello_world = `Hello, {name} from ${languageTag}!`; - fs.mkdirSync(fullDirectoryPath, { recursive: true }); - fs.writeFileSync(fullFilePath, JSON.stringify(jsonData, null, 2) + '\n'); + return generateCode(); + }); } }, + + // todo: to remove (start) + packages: [], + files: [], + // todo: to remove (end) + nextSteps: ({ highlighter }) => { const steps = [ `Edit your messages in ${highlighter.path('messages/en.json')}`, diff --git a/packages/core/adder/config.ts b/packages/core/adder/config.ts index f19cb146..b8207117 100644 --- a/packages/core/adder/config.ts +++ b/packages/core/adder/config.ts @@ -25,6 +25,22 @@ export type Scripts = { condition?: ConditionDefinition; }; +// todo: rename +export type FileApi = { + updateFile: (name: string, content: (content: string) => string) => string; +}; + +// todo: rename +export type PackageApi = { + dependency: (pkg: string, version: string) => void; + devDependency: (pkg: string, version: string) => void; +}; + +// todo: rename +export type ScriptApi = { + execute: (args: { description: string; args: string[]; stdio: 'inherit' | 'pipe' }) => void; +}; + export type Adder = { id: string; alias?: string; @@ -32,16 +48,23 @@ export type Adder = { homepage?: string; options: Args; dependsOn?: string[]; - packages: Array>; - scripts?: Array>; - files: Array>; - preInstall?: (workspace: Workspace) => MaybePromise; - postInstall?: (workspace: Workspace) => MaybePromise; nextSteps?: ( data: { highlighter: Highlighter; } & Workspace ) => string[]; + + run: ( + workspace: Workspace & { sv: FileApi & PackageApi & ScriptApi } + ) => MaybePromise; + + // todo: to remove (start) + files: Array>; + preInstall?: (workspace: Workspace) => MaybePromise; + postInstall?: (workspace: Workspace) => MaybePromise; + packages: Array>; + scripts?: Array>; + // todo: to remove (end) }; export type Highlighter = {