From f80470d5a048d01464f8744955e69ba84bc926e5 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 4 Dec 2025 14:21:31 +0800 Subject: [PATCH] feat: add support for custom tools --- package.json | 4 + pnpm-lock.yaml | 102 +++++++++++++++++- src/index.ts | 84 +++++++++++++-- test/custom-tools.test.ts | 96 +++++++++++++++++ test/fixtures/basic/package.json | 4 + .../basic/template-common/package.json | 4 + .../basic/template-vanilla/package.json | 4 + 7 files changed, 286 insertions(+), 12 deletions(-) create mode 100644 test/custom-tools.test.ts create mode 100644 test/fixtures/basic/package.json create mode 100644 test/fixtures/basic/template-common/package.json create mode 100644 test/fixtures/basic/template-vanilla/package.json diff --git a/package.json b/package.json index 35c8922..4a25686 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,13 @@ "@microsoft/api-extractor": "^7.55.1", "@rslib/core": "0.18.2", "@rstest/core": "0.6.8", + "@types/cross-spawn": "^6.0.6", + "@types/fs-extra": "^11.0.4", "@types/minimist": "^1.2.5", "@types/node": "24.10.1", + "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", + "fs-extra": "^11.3.2", "minimist": "^1.2.8", "picocolors": "^1.1.1", "rslog": "^1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75992b2..c097e07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,15 +23,27 @@ importers: '@rstest/core': specifier: 0.6.8 version: 0.6.8 + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 '@types/minimist': specifier: ^1.2.5 version: 1.2.5 '@types/node': specifier: 24.10.1 version: 24.10.1 + cross-spawn: + specifier: ^7.0.6 + version: 7.0.6 deepmerge: specifier: ^4.3.1 version: 4.3.1 + fs-extra: + specifier: ^11.3.2 + version: 11.3.2 minimist: specifier: ^1.2.8 version: 1.2.8 @@ -67,24 +79,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.37.0': resolution: {integrity: sha512-LF9sAvYy6es/OdyJDO3RwkX3I82Vkfsng1sqUBcoWC1jVb1wX5YVzHtpQox9JrEhGl+bNp7FYxB4Qba9OdA5GA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.37.0': resolution: {integrity: sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.37.0': resolution: {integrity: sha512-/BcCH33S9E3ovOAEoxYngUNXgb+JLg991sdyiNP2bSoYd30a9RHrG7CYwW6fMgua3ijQ474eV6cq9yZO1bCpXg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.37.0': resolution: {integrity: sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A==} @@ -130,24 +146,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.3.8': resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.3.8': resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.3.8': resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.3.8': resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==} @@ -283,41 +303,49 @@ packages: resolution: {integrity: sha512-UyUoh5RXHTWCktqPVnqoc5rwlWyLkWqGu6ga+iyJHDxdxlrHFfwJnTSnCd4y8cRadf7CrmjHElxE61GU3WCYhw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-gnu@1.6.5': resolution: {integrity: sha512-K68YDoV2e4s+nlrKZxgF0HehiiRwOAGgZFUwJNRMZ7MUrTGMNlPTJlM+bNdaCjDb6GFxBVFcNwIa1sU+0tF1zg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-musl@1.6.0-beta.1': resolution: {integrity: sha512-JAXVKHQieN4Ruvs7MstvsPUtRBSAROqJ0abCh4rXdV+FzncKp/ZkdfjQploDhBWtWfU8rPvIjaxeZcPfHMI5/A==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-arm64-musl@1.6.5': resolution: {integrity: sha512-JPtxFBOq7RRmBIwpdGIStf8iyCILehDsjQtEB0Kkhtm7TsAkVGwtC41GLcNuPxcQBKqNDmD8cy3yLYhXadH2CQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-gnu@1.6.0-beta.1': resolution: {integrity: sha512-LqAos71CJS5/V4knX9T7T68oGz0XPRZ2IJmI3jEByRlNcyZdxYeQ7Dw09JO9Y5Xj0T+0cudOeL2MxHcD3gTF/w==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-gnu@1.6.5': resolution: {integrity: sha512-oh4ZNo2HtizZ/E6UK3BEONu20h8VVBw9GAXuWmo1u22cJSihzg+WfRNCMjRDil82LqSsyAgBwnU+dEjEYGKyAA==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-musl@1.6.0-beta.1': resolution: {integrity: sha512-E4dRMzIHYaoYkgmDTFLrgnGtdspbAuVbLfaPF9AWW5YkQn52obGAgbbNb1wi1JJ5f29nTBoLauYCucEO5IGFvA==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-musl@1.6.5': resolution: {integrity: sha512-8Xebp5bvPJqjifpkFEAX5nUvoU2JvbMU3gwAkEovRRuvooCXnVT2tqkUBjkR3AhivAGgAxAr9hRzUUz/6QWt3Q==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-wasm32-wasi@1.6.0-beta.1': resolution: {integrity: sha512-PaKEjXOkYprSFlgdgVm/P3pv2E8nAQx9WSGgPmMVIAtxo3Cyz0wwFf0f1Bp9wCw0KkIWgi+9lz8oXNkgKZilug==} @@ -443,9 +471,18 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} @@ -487,6 +524,10 @@ packages: core-js@3.47.0: resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -498,8 +539,8 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fs-extra@11.3.0: - resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} function-bind@1.1.2: @@ -524,6 +565,9 @@ packages: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -551,6 +595,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -593,6 +641,14 @@ packages: engines: {node: '>=10'} hasBin: true + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + simple-git-hooks@2.13.1: resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==} hasBin: true @@ -650,6 +706,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -1002,7 +1063,7 @@ snapshots: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 11.3.0 + fs-extra: 11.3.2 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.8 @@ -1052,8 +1113,21 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 24.10.1 + '@types/deep-eql@4.0.2': {} + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 24.10.1 + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 24.10.1 + '@types/minimist@1.2.5': {} '@types/node@24.10.1': @@ -1092,13 +1166,19 @@ snapshots: core-js@3.47.0: {} + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + deepmerge@4.3.1: {} diff@8.0.2: {} fast-deep-equal@3.1.3: {} - fs-extra@11.3.0: + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 @@ -1120,6 +1200,8 @@ snapshots: dependencies: hasown: 2.0.2 + isexe@2.0.0: {} + jiti@2.6.1: {} jju@1.4.0: {} @@ -1144,6 +1226,8 @@ snapshots: minimist@1.2.8: {} + path-key@3.1.1: {} + path-parse@1.0.7: {} picocolors@1.1.1: {} @@ -1172,6 +1256,12 @@ snapshots: dependencies: lru-cache: 6.0.0 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + simple-git-hooks@2.13.1: {} sisteransi@1.0.5: {} @@ -1206,4 +1296,8 @@ snapshots: dependencies: punycode: 2.3.1 + which@2.0.2: + dependencies: + isexe: 2.0.0 + yallist@4.0.0: {} diff --git a/src/index.ts b/src/index.ts index 8a8a7bc..0a7b80b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { select, text, } from '@clack/prompts'; +import spawn from 'cross-spawn'; import deepmerge from 'deepmerge'; import minimist from 'minimist'; import color from 'picocolors'; @@ -104,10 +105,18 @@ function logHelpMessage(name: string, templates: string[]) { `); } -async function getTools({ tools, dir, template }: Argv) { +async function getTools( + { tools, dir, template }: Argv, + extraTools?: ExtraTool[], +) { + // Check if tools are specified via CLI options if (tools) { let toolsArr = Array.isArray(tools) ? tools : [tools]; - toolsArr = toolsArr.filter((tool) => BUILTIN_TOOLS.includes(tool)); + toolsArr = toolsArr.filter( + (tool) => + BUILTIN_TOOLS.includes(tool) || + extraTools?.some((extraTool) => extraTool.value === tool), + ); return toolsArr; } // skip tools selection when using CLI options @@ -119,15 +128,27 @@ async function getTools({ tools, dir, template }: Argv) { return []; } + const options = [ + { value: 'biome', label: 'Add Biome for code linting and formatting' }, + { value: 'eslint', label: 'Add ESLint for code linting' }, + { value: 'prettier', label: 'Add Prettier for code formatting' }, + ]; + + if (extraTools) { + options.push( + ...extraTools.map((tool) => ({ + value: tool.value, + label: tool.label, + hint: tool.command, + })), + ); + } + return checkCancel( await multiselect({ message: 'Select additional tools (Use to select, to continue)', - options: [ - { value: 'biome', label: 'Add Biome for code linting and formatting' }, - { value: 'eslint', label: 'Add ESLint for code linting' }, - { value: 'prettier', label: 'Add Prettier for code formatting' }, - ], + options, required: false, }), ); @@ -170,6 +191,33 @@ const parseArgv = () => { return argv; }; +type ExtraTool = { + /** + * The value of the multiselect option. + */ + value: string; + /** + * The label of the multiselect option. + */ + label: string; + /** + * The action to perform when the tool is selected. + */ + action?: () => unknown; + /** + * The custom command to run when the tool is selected. + */ + command?: string; +}; + +async function runCommand(command: string, cwd: string) { + const [bin, ...args] = command.split(' '); + spawn.sync(bin, args, { + stdio: 'inherit', + cwd, + }); +} + export async function create({ name, root, @@ -179,6 +227,7 @@ export async function create({ mapESLintTemplate, version, noteInformation, + extraTools, }: { name: string; root: string; @@ -191,6 +240,10 @@ export async function create({ ) => ESLintTemplateName | null; version?: Record | string; noteInformation?: string[]; + /** + * Specify additional tools. + */ + extraTools?: ExtraTool[]; }) { console.log(''); logger.greet(`◆ Create ${upperFirst(name)} Project`); @@ -251,7 +304,7 @@ export async function create({ } const templateName = await getTemplateName(argv); - const tools = await getTools(argv); + const tools = await getTools(argv, extraTools); const srcFolder = path.join(root, `template-${templateName}`); const commonFolder = path.join(root, 'template-common'); @@ -280,6 +333,21 @@ export async function create({ const agentsMdSearchDirs = [commonFolder, srcFolder]; for (const tool of tools) { + // Handle extra tools first + if (extraTools) { + const matchedTool = extraTools.find( + (extraTool) => extraTool.value === tool, + ); + if (matchedTool?.action) { + await matchedTool.action(); + } + if (matchedTool?.command) { + await runCommand(matchedTool.command, distFolder); + } + continue; + } + + // Handle built-in tools const toolFolder = path.join(packageRoot, `template-${tool}`); if (tool === 'eslint') { diff --git a/test/custom-tools.test.ts b/test/custom-tools.test.ts new file mode 100644 index 0000000..d57178f --- /dev/null +++ b/test/custom-tools.test.ts @@ -0,0 +1,96 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { assert, beforeEach, test } from '@rstest/core'; +import fse from 'fs-extra'; +import { create } from '../dist/index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const testDir = path.join(__dirname, 'temp'); +const fixturesDir = path.join(__dirname, 'fixtures', 'basic'); + +beforeEach(() => { + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true }); + } + fs.mkdirSync(testDir, { recursive: true }); + + const originalArgv = process.argv; + + return () => { + process.argv = originalArgv; + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true }); + } + }; +}); + +test('should run extra tool action', async () => { + const projectDir = path.join(testDir, 'extra-tool-action'); + let actionCalled = false; + + process.argv = [ + 'node', + 'test', + '--dir', + projectDir, + '--template', + 'vanilla', + '--tools', + 'custom-action', + ]; + + await create({ + name: 'test', + root: fixturesDir, + templates: ['vanilla'], + getTemplateName: async () => 'vanilla', + mapESLintTemplate: () => null, + extraTools: [ + { + value: 'custom-action', + label: 'Custom Action', + action: () => { + actionCalled = true; + }, + }, + ], + }); + + assert.strictEqual(actionCalled, true); +}); + +test('should run extra tool command', async () => { + const projectDir = path.join(testDir, 'extra-tool-command'); + const touchedFile = path.join(projectDir, 'touched-by-command.txt'); + + await fse.remove(touchedFile); + + process.argv = [ + 'node', + 'test', + '--dir', + projectDir, + '--template', + 'vanilla', + '--tools', + 'custom-command', + ]; + + await create({ + name: 'test', + root: fixturesDir, + templates: ['vanilla'], + getTemplateName: async () => 'vanilla', + mapESLintTemplate: () => null, + extraTools: [ + { + value: 'custom-command', + label: 'Custom Command', + command: 'touch touched-by-command.txt', + }, + ], + }); + + assert.strictEqual(fs.existsSync(touchedFile), true); +}); diff --git a/test/fixtures/basic/package.json b/test/fixtures/basic/package.json new file mode 100644 index 0000000..262594f --- /dev/null +++ b/test/fixtures/basic/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-fixtures-basic", + "version": "1.0.0" +} diff --git a/test/fixtures/basic/template-common/package.json b/test/fixtures/basic/template-common/package.json new file mode 100644 index 0000000..f4138df --- /dev/null +++ b/test/fixtures/basic/template-common/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-common", + "version": "1.0.0" +} diff --git a/test/fixtures/basic/template-vanilla/package.json b/test/fixtures/basic/template-vanilla/package.json new file mode 100644 index 0000000..65651ce --- /dev/null +++ b/test/fixtures/basic/template-vanilla/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-vanilla", + "version": "1.0.0" +}